From 29c342b020f9bf5fae11c62380ace326c954a51b Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 18 Nov 2020 10:08:50 -0500 Subject: [PATCH] remove unused helper packages helper/schema has been moved into internal/legacy/helper. helper/resource and helper/validation are no longer used. --- helper/resource/error.go | 79 - helper/resource/grpc_test_provider.go | 43 - helper/resource/id.go | 45 - helper/resource/id_test.go | 66 - helper/resource/state.go | 259 - helper/resource/state_shim.go | 218 - helper/resource/state_shim_test.go | 387 -- helper/resource/state_test.go | 329 -- helper/resource/testing.go | 1285 ---- helper/resource/testing_config.go | 378 -- helper/resource/testing_import_state.go | 230 - helper/resource/testing_import_state_test.go | 517 -- helper/resource/testing_test.go | 1361 ----- helper/resource/wait.go | 84 - helper/resource/wait_test.go | 95 - helper/schema/README.md | 11 - helper/schema/backend.go | 200 - helper/schema/backend_test.go | 193 - helper/schema/core_schema.go | 309 - helper/schema/core_schema_test.go | 458 -- helper/schema/data_source_resource_shim.go | 59 - helper/schema/equal.go | 6 - helper/schema/field_reader.go | 343 -- helper/schema/field_reader_config.go | 353 -- helper/schema/field_reader_config_test.go | 540 -- helper/schema/field_reader_diff.go | 244 - helper/schema/field_reader_diff_test.go | 524 -- helper/schema/field_reader_map.go | 235 - helper/schema/field_reader_map_test.go | 123 - helper/schema/field_reader_multi.go | 63 - helper/schema/field_reader_multi_test.go | 270 - helper/schema/field_reader_test.go | 471 -- helper/schema/field_writer.go | 8 - helper/schema/field_writer_map.go | 357 -- helper/schema/field_writer_map_test.go | 547 -- helper/schema/getsource_string.go | 46 - helper/schema/provider.go | 477 -- helper/schema/provider_test.go | 620 -- helper/schema/provisioner.go | 205 - helper/schema/provisioner_test.go | 334 -- helper/schema/resource.go | 842 --- helper/schema/resource_data.go | 561 -- helper/schema/resource_data_get_source.go | 17 - helper/schema/resource_data_test.go | 3564 ----------- helper/schema/resource_diff.go | 559 -- helper/schema/resource_diff_test.go | 2045 ------- helper/schema/resource_importer.go | 52 - helper/schema/resource_test.go | 1687 ------ helper/schema/resource_timeout.go | 263 - helper/schema/resource_timeout_test.go | 376 -- helper/schema/schema.go | 1854 ------ helper/schema/schema_test.go | 5558 ------------------ helper/schema/serialize.go | 125 - helper/schema/serialize_test.go | 238 - helper/schema/set.go | 250 - helper/schema/set_test.go | 217 - helper/schema/shims.go | 115 - helper/schema/shims_test.go | 3521 ----------- helper/schema/testing.go | 28 - helper/schema/valuetype.go | 21 - helper/schema/valuetype_string.go | 31 - helper/validation/validation.go | 49 - helper/validation/validation_test.go | 95 - 63 files changed, 34440 deletions(-) delete mode 100644 helper/resource/error.go delete mode 100644 helper/resource/grpc_test_provider.go delete mode 100644 helper/resource/id.go delete mode 100644 helper/resource/id_test.go delete mode 100644 helper/resource/state.go delete mode 100644 helper/resource/state_shim.go delete mode 100644 helper/resource/state_shim_test.go delete mode 100644 helper/resource/state_test.go delete mode 100644 helper/resource/testing.go delete mode 100644 helper/resource/testing_config.go delete mode 100644 helper/resource/testing_import_state.go delete mode 100644 helper/resource/testing_import_state_test.go delete mode 100644 helper/resource/testing_test.go delete mode 100644 helper/resource/wait.go delete mode 100644 helper/resource/wait_test.go delete mode 100644 helper/schema/README.md delete mode 100644 helper/schema/backend.go delete mode 100644 helper/schema/backend_test.go delete mode 100644 helper/schema/core_schema.go delete mode 100644 helper/schema/core_schema_test.go delete mode 100644 helper/schema/data_source_resource_shim.go delete mode 100644 helper/schema/equal.go delete mode 100644 helper/schema/field_reader.go delete mode 100644 helper/schema/field_reader_config.go delete mode 100644 helper/schema/field_reader_config_test.go delete mode 100644 helper/schema/field_reader_diff.go delete mode 100644 helper/schema/field_reader_diff_test.go delete mode 100644 helper/schema/field_reader_map.go delete mode 100644 helper/schema/field_reader_map_test.go delete mode 100644 helper/schema/field_reader_multi.go delete mode 100644 helper/schema/field_reader_multi_test.go delete mode 100644 helper/schema/field_reader_test.go delete mode 100644 helper/schema/field_writer.go delete mode 100644 helper/schema/field_writer_map.go delete mode 100644 helper/schema/field_writer_map_test.go delete mode 100644 helper/schema/getsource_string.go delete mode 100644 helper/schema/provider.go delete mode 100644 helper/schema/provider_test.go delete mode 100644 helper/schema/provisioner.go delete mode 100644 helper/schema/provisioner_test.go delete mode 100644 helper/schema/resource.go delete mode 100644 helper/schema/resource_data.go delete mode 100644 helper/schema/resource_data_get_source.go delete mode 100644 helper/schema/resource_data_test.go delete mode 100644 helper/schema/resource_diff.go delete mode 100644 helper/schema/resource_diff_test.go delete mode 100644 helper/schema/resource_importer.go delete mode 100644 helper/schema/resource_test.go delete mode 100644 helper/schema/resource_timeout.go delete mode 100644 helper/schema/resource_timeout_test.go delete mode 100644 helper/schema/schema.go delete mode 100644 helper/schema/schema_test.go delete mode 100644 helper/schema/serialize.go delete mode 100644 helper/schema/serialize_test.go delete mode 100644 helper/schema/set.go delete mode 100644 helper/schema/set_test.go delete mode 100644 helper/schema/shims.go delete mode 100644 helper/schema/shims_test.go delete mode 100644 helper/schema/testing.go delete mode 100644 helper/schema/valuetype.go delete mode 100644 helper/schema/valuetype_string.go delete mode 100644 helper/validation/validation.go delete mode 100644 helper/validation/validation_test.go diff --git a/helper/resource/error.go b/helper/resource/error.go deleted file mode 100644 index 7ee21614b..000000000 --- a/helper/resource/error.go +++ /dev/null @@ -1,79 +0,0 @@ -package resource - -import ( - "fmt" - "strings" - "time" -) - -type NotFoundError struct { - LastError error - LastRequest interface{} - LastResponse interface{} - Message string - Retries int -} - -func (e *NotFoundError) Error() string { - if e.Message != "" { - return e.Message - } - - if e.Retries > 0 { - return fmt.Sprintf("couldn't find resource (%d retries)", e.Retries) - } - - return "couldn't find resource" -} - -// UnexpectedStateError is returned when Refresh returns a state that's neither in Target nor Pending -type UnexpectedStateError struct { - LastError error - State string - ExpectedState []string -} - -func (e *UnexpectedStateError) Error() string { - return fmt.Sprintf( - "unexpected state '%s', wanted target '%s'. last error: %s", - e.State, - strings.Join(e.ExpectedState, ", "), - e.LastError, - ) -} - -// TimeoutError is returned when WaitForState times out -type TimeoutError struct { - LastError error - LastState string - Timeout time.Duration - ExpectedState []string -} - -func (e *TimeoutError) Error() string { - expectedState := "resource to be gone" - if len(e.ExpectedState) > 0 { - expectedState = fmt.Sprintf("state to become '%s'", strings.Join(e.ExpectedState, ", ")) - } - - extraInfo := make([]string, 0) - if e.LastState != "" { - extraInfo = append(extraInfo, fmt.Sprintf("last state: '%s'", e.LastState)) - } - if e.Timeout > 0 { - extraInfo = append(extraInfo, fmt.Sprintf("timeout: %s", e.Timeout.String())) - } - - suffix := "" - if len(extraInfo) > 0 { - suffix = fmt.Sprintf(" (%s)", strings.Join(extraInfo, ", ")) - } - - if e.LastError != nil { - return fmt.Sprintf("timeout while waiting for %s%s: %s", - expectedState, suffix, e.LastError) - } - - return fmt.Sprintf("timeout while waiting for %s%s", - expectedState, suffix) -} diff --git a/helper/resource/grpc_test_provider.go b/helper/resource/grpc_test_provider.go deleted file mode 100644 index 0742e993b..000000000 --- a/helper/resource/grpc_test_provider.go +++ /dev/null @@ -1,43 +0,0 @@ -package resource - -import ( - "context" - "net" - "time" - - "github.com/hashicorp/terraform/helper/plugin" - proto "github.com/hashicorp/terraform/internal/tfplugin5" - tfplugin "github.com/hashicorp/terraform/plugin" - "github.com/hashicorp/terraform/providers" - "github.com/hashicorp/terraform/terraform" - "google.golang.org/grpc" - "google.golang.org/grpc/test/bufconn" -) - -// GRPCTestProvider takes a legacy ResourceProvider, wraps it in the new GRPC -// shim and starts it in a grpc server using an inmem connection. It returns a -// GRPCClient for this new server to test the shimmed resource provider. -func GRPCTestProvider(rp terraform.ResourceProvider) providers.Interface { - listener := bufconn.Listen(256 * 1024) - grpcServer := grpc.NewServer() - - p := plugin.NewGRPCProviderServerShim(rp) - proto.RegisterProviderServer(grpcServer, p) - - go grpcServer.Serve(listener) - - conn, err := grpc.Dial("", grpc.WithDialer(func(string, time.Duration) (net.Conn, error) { - return listener.Dial() - }), grpc.WithInsecure()) - if err != nil { - panic(err) - } - - var pp tfplugin.GRPCProviderPlugin - client, _ := pp.GRPCClient(context.Background(), nil, conn) - - grpcClient := client.(*tfplugin.GRPCProvider) - grpcClient.TestServer = grpcServer - - return grpcClient -} diff --git a/helper/resource/id.go b/helper/resource/id.go deleted file mode 100644 index 44949550e..000000000 --- a/helper/resource/id.go +++ /dev/null @@ -1,45 +0,0 @@ -package resource - -import ( - "fmt" - "strings" - "sync" - "time" -) - -const UniqueIdPrefix = `terraform-` - -// idCounter is a monotonic counter for generating ordered unique ids. -var idMutex sync.Mutex -var idCounter uint32 - -// Helper for a resource to generate a unique identifier w/ default prefix -func UniqueId() string { - return PrefixedUniqueId(UniqueIdPrefix) -} - -// UniqueIDSuffixLength is the string length of the suffix generated by -// PrefixedUniqueId. This can be used by length validation functions to -// ensure prefixes are the correct length for the target field. -const UniqueIDSuffixLength = 26 - -// Helper for a resource to generate a unique identifier w/ given prefix -// -// After the prefix, the ID consists of an incrementing 26 digit value (to match -// previous timestamp output). After the prefix, the ID consists of a timestamp -// and an incrementing 8 hex digit value The timestamp means that multiple IDs -// created with the same prefix will sort in the order of their creation, even -// across multiple terraform executions, as long as the clock is not turned back -// between calls, and as long as any given terraform execution generates fewer -// than 4 billion IDs. -func PrefixedUniqueId(prefix string) string { - // Be precise to 4 digits of fractional seconds, but remove the dot before the - // fractional seconds. - timestamp := strings.Replace( - time.Now().UTC().Format("20060102150405.0000"), ".", "", 1) - - idMutex.Lock() - defer idMutex.Unlock() - idCounter++ - return fmt.Sprintf("%s%s%08x", prefix, timestamp, idCounter) -} diff --git a/helper/resource/id_test.go b/helper/resource/id_test.go deleted file mode 100644 index f1560dab1..000000000 --- a/helper/resource/id_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package resource - -import ( - "regexp" - "strings" - "testing" - "time" -) - -var allDigits = regexp.MustCompile(`^\d+$`) -var allHex = regexp.MustCompile(`^[a-f0-9]+$`) - -func TestUniqueId(t *testing.T) { - split := func(rest string) (timestamp, increment string) { - return rest[:18], rest[18:] - } - - iterations := 10000 - ids := make(map[string]struct{}) - var id, lastId string - for i := 0; i < iterations; i++ { - id = UniqueId() - - if _, ok := ids[id]; ok { - t.Fatalf("Got duplicated id! %s", id) - } - - if !strings.HasPrefix(id, UniqueIdPrefix) { - t.Fatalf("Unique ID didn't have terraform- prefix! %s", id) - } - - rest := strings.TrimPrefix(id, UniqueIdPrefix) - - if len(rest) != UniqueIDSuffixLength { - t.Fatalf("PrefixedUniqueId is out of sync with UniqueIDSuffixLength, post-prefix part has wrong length! %s", rest) - } - - timestamp, increment := split(rest) - - if !allDigits.MatchString(timestamp) { - t.Fatalf("Timestamp not all digits! %s", timestamp) - } - - if !allHex.MatchString(increment) { - t.Fatalf("Increment part not all hex! %s", increment) - } - - if lastId != "" && lastId >= id { - t.Fatalf("IDs not ordered! %s vs %s", lastId, id) - } - - ids[id] = struct{}{} - lastId = id - } - - id1 := UniqueId() - time.Sleep(time.Millisecond) - id2 := UniqueId() - timestamp1, _ := split(strings.TrimPrefix(id1, UniqueIdPrefix)) - timestamp2, _ := split(strings.TrimPrefix(id2, UniqueIdPrefix)) - - if timestamp1 == timestamp2 { - t.Fatalf("Timestamp part should update at least once a millisecond %s %s", - id1, id2) - } -} diff --git a/helper/resource/state.go b/helper/resource/state.go deleted file mode 100644 index 88a839664..000000000 --- a/helper/resource/state.go +++ /dev/null @@ -1,259 +0,0 @@ -package resource - -import ( - "log" - "time" -) - -var refreshGracePeriod = 30 * time.Second - -// StateRefreshFunc is a function type used for StateChangeConf that is -// responsible for refreshing the item being watched for a state change. -// -// It returns three results. `result` is any object that will be returned -// as the final object after waiting for state change. This allows you to -// return the final updated object, for example an EC2 instance after refreshing -// it. -// -// `state` is the latest state of that object. And `err` is any error that -// may have happened while refreshing the state. -type StateRefreshFunc func() (result interface{}, state string, err error) - -// StateChangeConf is the configuration struct used for `WaitForState`. -type StateChangeConf struct { - Delay time.Duration // Wait this time before starting checks - Pending []string // States that are "allowed" and will continue trying - Refresh StateRefreshFunc // Refreshes the current state - Target []string // Target state - Timeout time.Duration // The amount of time to wait before timeout - MinTimeout time.Duration // Smallest time to wait before refreshes - PollInterval time.Duration // Override MinTimeout/backoff and only poll this often - NotFoundChecks int // Number of times to allow not found - - // This is to work around inconsistent APIs - ContinuousTargetOccurence int // Number of times the Target state has to occur continuously -} - -// WaitForState watches an object and waits for it to achieve the state -// specified in the configuration using the specified Refresh() func, -// waiting the number of seconds specified in the timeout configuration. -// -// If the Refresh function returns an error, exit immediately with that error. -// -// If the Refresh function returns a state other than the Target state or one -// listed in Pending, return immediately with an error. -// -// If the Timeout is exceeded before reaching the Target state, return an -// error. -// -// Otherwise, the result is the result of the first call to the Refresh function to -// reach the target state. -func (conf *StateChangeConf) WaitForState() (interface{}, error) { - log.Printf("[DEBUG] Waiting for state to become: %s", conf.Target) - - notfoundTick := 0 - targetOccurence := 0 - - // Set a default for times to check for not found - if conf.NotFoundChecks == 0 { - conf.NotFoundChecks = 20 - } - - if conf.ContinuousTargetOccurence == 0 { - conf.ContinuousTargetOccurence = 1 - } - - type Result struct { - Result interface{} - State string - Error error - Done bool - } - - // Read every result from the refresh loop, waiting for a positive result.Done. - resCh := make(chan Result, 1) - // cancellation channel for the refresh loop - cancelCh := make(chan struct{}) - - result := Result{} - - go func() { - defer close(resCh) - - time.Sleep(conf.Delay) - - // start with 0 delay for the first loop - var wait time.Duration - - for { - // store the last result - resCh <- result - - // wait and watch for cancellation - select { - case <-cancelCh: - return - case <-time.After(wait): - // first round had no wait - if wait == 0 { - wait = 100 * time.Millisecond - } - } - - res, currentState, err := conf.Refresh() - result = Result{ - Result: res, - State: currentState, - Error: err, - } - - if err != nil { - resCh <- result - return - } - - // If we're waiting for the absence of a thing, then return - if res == nil && len(conf.Target) == 0 { - targetOccurence++ - if conf.ContinuousTargetOccurence == targetOccurence { - result.Done = true - resCh <- result - return - } - continue - } - - if res == nil { - // If we didn't find the resource, check if we have been - // not finding it for awhile, and if so, report an error. - notfoundTick++ - if notfoundTick > conf.NotFoundChecks { - result.Error = &NotFoundError{ - LastError: err, - Retries: notfoundTick, - } - resCh <- result - return - } - } else { - // Reset the counter for when a resource isn't found - notfoundTick = 0 - found := false - - for _, allowed := range conf.Target { - if currentState == allowed { - found = true - targetOccurence++ - if conf.ContinuousTargetOccurence == targetOccurence { - result.Done = true - resCh <- result - return - } - continue - } - } - - for _, allowed := range conf.Pending { - if currentState == allowed { - found = true - targetOccurence = 0 - break - } - } - - if !found && len(conf.Pending) > 0 { - result.Error = &UnexpectedStateError{ - LastError: err, - State: result.State, - ExpectedState: conf.Target, - } - resCh <- result - return - } - } - - // Wait between refreshes using exponential backoff, except when - // waiting for the target state to reoccur. - if targetOccurence == 0 { - wait *= 2 - } - - // If a poll interval has been specified, choose that interval. - // Otherwise bound the default value. - if conf.PollInterval > 0 && conf.PollInterval < 180*time.Second { - wait = conf.PollInterval - } else { - if wait < conf.MinTimeout { - wait = conf.MinTimeout - } else if wait > 10*time.Second { - wait = 10 * time.Second - } - } - - log.Printf("[TRACE] Waiting %s before next try", wait) - } - }() - - // store the last value result from the refresh loop - lastResult := Result{} - - timeout := time.After(conf.Timeout) - for { - select { - case r, ok := <-resCh: - // channel closed, so return the last result - if !ok { - return lastResult.Result, lastResult.Error - } - - // we reached the intended state - if r.Done { - return r.Result, r.Error - } - - // still waiting, store the last result - lastResult = r - - case <-timeout: - log.Printf("[WARN] WaitForState timeout after %s", conf.Timeout) - log.Printf("[WARN] WaitForState starting %s refresh grace period", refreshGracePeriod) - - // cancel the goroutine and start our grace period timer - close(cancelCh) - timeout := time.After(refreshGracePeriod) - - // we need a for loop and a label to break on, because we may have - // an extra response value to read, but still want to wait for the - // channel to close. - forSelect: - for { - select { - case r, ok := <-resCh: - if r.Done { - // the last refresh loop reached the desired state - return r.Result, r.Error - } - - if !ok { - // the goroutine returned - break forSelect - } - - // target state not reached, save the result for the - // TimeoutError and wait for the channel to close - lastResult = r - case <-timeout: - log.Println("[ERROR] WaitForState exceeded refresh grace period") - break forSelect - } - } - - return nil, &TimeoutError{ - LastError: lastResult.Error, - LastState: lastResult.State, - Timeout: conf.Timeout, - ExpectedState: conf.Target, - } - } - } -} diff --git a/helper/resource/state_shim.go b/helper/resource/state_shim.go deleted file mode 100644 index aa2231b28..000000000 --- a/helper/resource/state_shim.go +++ /dev/null @@ -1,218 +0,0 @@ -package resource - -import ( - "encoding/json" - "fmt" - - "github.com/hashicorp/terraform/addrs" - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/helper/schema" - - "github.com/hashicorp/terraform/states" - "github.com/hashicorp/terraform/terraform" -) - -// shimState takes a new *states.State and reverts it to a legacy state for the provider ACC tests -func shimNewState(newState *states.State, providers map[string]terraform.ResourceProvider) (*terraform.State, error) { - state := terraform.NewState() - - // in the odd case of a nil state, let the helper packages handle it - if newState == nil { - return nil, nil - } - - for _, newMod := range newState.Modules { - mod := state.AddModule(newMod.Addr) - - for name, out := range newMod.OutputValues { - outputType := "" - val := hcl2shim.ConfigValueFromHCL2(out.Value) - ty := out.Value.Type() - switch { - case ty == cty.String: - outputType = "string" - case ty.IsTupleType() || ty.IsListType(): - outputType = "list" - case ty.IsMapType(): - outputType = "map" - } - - mod.Outputs[name] = &terraform.OutputState{ - Type: outputType, - Value: val, - Sensitive: out.Sensitive, - } - } - - for _, res := range newMod.Resources { - resType := res.Addr.Resource.Type - providerType := res.ProviderConfig.Provider.Type - - resource := getResource(providers, providerType, res.Addr.Resource) - - for key, i := range res.Instances { - resState := &terraform.ResourceState{ - Type: resType, - Provider: legacyProviderConfigString(res.ProviderConfig), - } - - // We should always have a Current instance here, but be safe about checking. - if i.Current != nil { - flatmap, err := shimmedAttributes(i.Current, resource) - if err != nil { - return nil, fmt.Errorf("error decoding state for %q: %s", resType, err) - } - - var meta map[string]interface{} - if i.Current.Private != nil { - err := json.Unmarshal(i.Current.Private, &meta) - if err != nil { - return nil, err - } - } - - resState.Primary = &terraform.InstanceState{ - ID: flatmap["id"], - Attributes: flatmap, - Tainted: i.Current.Status == states.ObjectTainted, - Meta: meta, - } - - if i.Current.SchemaVersion != 0 { - if resState.Primary.Meta == nil { - resState.Primary.Meta = map[string]interface{}{} - } - resState.Primary.Meta["schema_version"] = i.Current.SchemaVersion - } - - // convert the indexes to the old style flapmap indexes - idx := "" - switch key.(type) { - case addrs.IntKey: - // don't add numeric index values to resources with a count of 0 - if len(res.Instances) > 1 { - idx = fmt.Sprintf(".%d", key) - } - case addrs.StringKey: - idx = "." + key.String() - } - - mod.Resources[res.Addr.Resource.String()+idx] = resState - } - - // add any deposed instances - for _, dep := range i.Deposed { - flatmap, err := shimmedAttributes(dep, resource) - if err != nil { - return nil, fmt.Errorf("error decoding deposed state for %q: %s", resType, err) - } - - var meta map[string]interface{} - if dep.Private != nil { - err := json.Unmarshal(dep.Private, &meta) - if err != nil { - return nil, err - } - } - - deposed := &terraform.InstanceState{ - ID: flatmap["id"], - Attributes: flatmap, - Tainted: dep.Status == states.ObjectTainted, - Meta: meta, - } - if dep.SchemaVersion != 0 { - deposed.Meta = map[string]interface{}{ - "schema_version": dep.SchemaVersion, - } - } - - resState.Deposed = append(resState.Deposed, deposed) - } - } - } - } - - return state, nil -} - -func getResource(providers map[string]terraform.ResourceProvider, providerName string, addr addrs.Resource) *schema.Resource { - p := providers[providerName] - if p == nil { - panic(fmt.Sprintf("provider %q not found in test step", providerName)) - } - - // this is only for tests, so should only see schema.Providers - provider := p.(*schema.Provider) - - switch addr.Mode { - case addrs.ManagedResourceMode: - resource := provider.ResourcesMap[addr.Type] - if resource != nil { - return resource - } - case addrs.DataResourceMode: - resource := provider.DataSourcesMap[addr.Type] - if resource != nil { - return resource - } - } - - panic(fmt.Sprintf("resource %s not found in test step", addr.Type)) -} - -func shimmedAttributes(instance *states.ResourceInstanceObjectSrc, res *schema.Resource) (map[string]string, error) { - flatmap := instance.AttrsFlat - if flatmap != nil { - return flatmap, nil - } - - // if we have json attrs, they need to be decoded - rio, err := instance.Decode(res.CoreConfigSchema().ImpliedType()) - if err != nil { - return nil, err - } - - instanceState, err := res.ShimInstanceStateFromValue(rio.Value) - if err != nil { - return nil, err - } - - return instanceState.Attributes, nil -} - -func shimLegacyState(legacy *terraform.State) (*states.State, error) { - state, err := terraform.ShimLegacyState(legacy) - if err != nil { - return nil, err - } - - if state.HasResources() { - for _, module := range state.Modules { - for name, resource := range module.Resources { - module.Resources[name].ProviderConfig.Provider = addrs.ImpliedProviderForUnqualifiedType(resource.Addr.Resource.ImpliedProvider()) - } - } - } - return state, err -} - -// legacyProviderConfigString was copied from addrs.Provider.LegacyString() to -// create a legacy-style string from a non-legacy provider. This is only -// necessary as this package shims back and forth between legacy and modern -// state, neither of which encode the addrs.Provider for a resource. -func legacyProviderConfigString(pc addrs.AbsProviderConfig) string { - if pc.Alias != "" { - if len(pc.Module) == 0 { - return fmt.Sprintf("%s.%s.%s", "provider", pc.Provider.Type, pc.Alias) - } else { - return fmt.Sprintf("%s.%s.%s.%s", pc.Module.String(), "provider", pc.Provider.LegacyString(), pc.Alias) - } - } - if len(pc.Module) == 0 { - return fmt.Sprintf("%s.%s", "provider", pc.Provider.Type) - } - return fmt.Sprintf("%s.%s.%s", pc.Module.String(), "provider", pc.Provider.Type) -} diff --git a/helper/resource/state_shim_test.go b/helper/resource/state_shim_test.go deleted file mode 100644 index 789e1295f..000000000 --- a/helper/resource/state_shim_test.go +++ /dev/null @@ -1,387 +0,0 @@ -package resource - -import ( - "testing" - - "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/states" - "github.com/hashicorp/terraform/terraform" - "github.com/zclconf/go-cty/cty" -) - -// TestStateShim is meant to be a fairly comprehensive test, checking for dependencies, root outputs, -func TestStateShim(t *testing.T) { - state := states.NewState() - - rootModule := state.RootModule() - if rootModule == nil { - t.Errorf("root module is nil; want valid object") - } - - rootModule.SetOutputValue("bar", cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("value")}), false) - rootModule.SetOutputValue("secret", cty.StringVal("secret value"), true) - rootModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "foo", - }.Instance(addrs.NoKey), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsFlat: map[string]string{"id": "foo", "bazzle": "dazzle"}, - SchemaVersion: 7, - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - rootModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "baz", - }.Instance(addrs.NoKey), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsFlat: map[string]string{"id": "baz", "bazzle": "dazzle"}, - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - - childInstance := addrs.RootModuleInstance.Child("child", addrs.NoKey) - childModule := state.EnsureModule(childInstance) - childModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.DataResourceMode, - Type: "test_data_thing", - Name: "foo", - }.Instance(addrs.NoKey), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsJSON: []byte(`{"id": "bar", "fuzzle":"wuzzle"}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: childInstance.Module(), - }, - ) - childModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "baz", - }.Instance(addrs.NoKey), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsJSON: []byte(`{"id": "bar", "fizzle":"wizzle"}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: childInstance.Module(), - }, - ) - - childModule.SetResourceInstanceDeposed( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "baz", - }.Instance(addrs.NoKey), - "00000001", - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsFlat: map[string]string{"id": "old", "fizzle": "wizzle"}, - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: childInstance.Module(), - }, - ) - - childModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "lots", - }.Instance(addrs.IntKey(0)), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsFlat: map[string]string{"id": "0", "bazzle": "dazzle"}, - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: childInstance.Module(), - }, - ) - childModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "lots", - }.Instance(addrs.IntKey(1)), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectTainted, - AttrsFlat: map[string]string{"id": "1", "bazzle": "dazzle"}, - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: childInstance.Module(), - }, - ) - - childModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "single_count", - }.Instance(addrs.IntKey(0)), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsJSON: []byte(`{"id": "single", "bazzle":"dazzle"}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: childInstance.Module(), - }, - ) - - expected := &terraform.State{ - Version: 3, - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Outputs: map[string]*terraform.OutputState{ - "bar": { - Type: "list", - Value: []interface{}{"bar", "value"}, - }, - "secret": { - Sensitive: true, - Type: "string", - Value: "secret value", - }, - }, - Resources: map[string]*terraform.ResourceState{ - "test_thing.baz": &terraform.ResourceState{ - Type: "test_thing", - Provider: "provider.test", - Primary: &terraform.InstanceState{ - ID: "baz", - Attributes: map[string]string{ - "id": "baz", - "bazzle": "dazzle", - }, - }, - }, - "test_thing.foo": &terraform.ResourceState{ - Type: "test_thing", - Provider: "provider.test", - Primary: &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "bazzle": "dazzle", - }, - Meta: map[string]interface{}{ - "schema_version": 7, - }, - }, - }, - }, - }, - &terraform.ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*terraform.ResourceState{ - "test_thing.baz": &terraform.ResourceState{ - Type: "test_thing", - Provider: "module.child.provider.test", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "fizzle": "wizzle", - }, - }, - Deposed: []*terraform.InstanceState{ - { - ID: "old", - Attributes: map[string]string{ - "id": "old", - "fizzle": "wizzle", - }, - }, - }, - }, - "data.test_data_thing.foo": &terraform.ResourceState{ - Type: "test_data_thing", - Provider: "module.child.provider.test", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "fuzzle": "wuzzle", - }, - }, - }, - "test_thing.lots.0": &terraform.ResourceState{ - Type: "test_thing", - Provider: "module.child.provider.test", - Primary: &terraform.InstanceState{ - ID: "0", - Attributes: map[string]string{ - "id": "0", - "bazzle": "dazzle", - }, - }, - }, - "test_thing.lots.1": &terraform.ResourceState{ - Type: "test_thing", - Provider: "module.child.provider.test", - Primary: &terraform.InstanceState{ - ID: "1", - Attributes: map[string]string{ - "id": "1", - "bazzle": "dazzle", - }, - Tainted: true, - }, - }, - "test_thing.single_count": &terraform.ResourceState{ - Type: "test_thing", - Provider: "module.child.provider.test", - Primary: &terraform.InstanceState{ - ID: "single", - Attributes: map[string]string{ - "id": "single", - "bazzle": "dazzle", - }, - }, - }, - }, - }, - }, - } - - providers := map[string]terraform.ResourceProvider{ - "test": &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "test_thing": &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": {Type: schema.TypeString, Computed: true}, - "fizzle": {Type: schema.TypeString, Optional: true}, - "bazzle": {Type: schema.TypeString, Optional: true}, - }, - }, - }, - DataSourcesMap: map[string]*schema.Resource{ - "test_data_thing": &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": {Type: schema.TypeString, Computed: true}, - "fuzzle": {Type: schema.TypeString, Optional: true}, - }, - }, - }, - }, - } - - shimmed, err := shimNewState(state, providers) - if err != nil { - t.Fatal(err) - } - - if !expected.Equal(shimmed) { - t.Fatalf("wrong result state\ngot:\n%s\n\nwant:\n%s", shimmed, expected) - } -} - -// TestShimLegacyState only checks the functionality unique to this func: adding -// the implied provider FQN -func TestShimLegacyState(t *testing.T) { - - input := &terraform.State{ - Version: 3, - Modules: []*terraform.ModuleState{ - &terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_thing.baz": &terraform.ResourceState{ - Type: "test_thing", - Provider: "provider.test", - Primary: &terraform.InstanceState{ - ID: "baz", - Attributes: map[string]string{ - "id": "baz", - "bazzle": "dazzle", - }, - }, - }, - }, - }, - &terraform.ModuleState{ - Path: []string{"root", "child"}, - Resources: map[string]*terraform.ResourceState{ - "test_thing.bar": &terraform.ResourceState{ - Type: "test_thing", - Provider: "module.child.provider.test", - Primary: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "fizzle": "wizzle", - }, - }, - }, - }, - }, - }, - } - - expected := states.NewState() - root := expected.EnsureModule(addrs.RootModuleInstance) - root.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "baz", - }.Instance(addrs.NoKey), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsFlat: map[string]string{"id": "baz", "bazzle": "dazzle"}, - Dependencies: []addrs.ConfigResource{}, - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - child := expected.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) - child.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "bar", - }.Instance(addrs.NoKey), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - AttrsFlat: map[string]string{"id": "bar", "fizzle": "wizzle"}, - Dependencies: []addrs.ConfigResource{}, - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: child.Addr.Module(), - }, - ) - - got, err := shimLegacyState(input) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !got.Equal(expected) { - t.Fatal("wrong result") - } -} diff --git a/helper/resource/state_test.go b/helper/resource/state_test.go deleted file mode 100644 index 6d6b329a1..000000000 --- a/helper/resource/state_test.go +++ /dev/null @@ -1,329 +0,0 @@ -package resource - -import ( - "errors" - "strings" - "sync/atomic" - "testing" - "time" -) - -func FailedStateRefreshFunc() StateRefreshFunc { - return func() (interface{}, string, error) { - return nil, "", errors.New("failed") - } -} - -func TimeoutStateRefreshFunc() StateRefreshFunc { - return func() (interface{}, string, error) { - time.Sleep(100 * time.Second) - return nil, "", errors.New("failed") - } -} - -func SuccessfulStateRefreshFunc() StateRefreshFunc { - return func() (interface{}, string, error) { - return struct{}{}, "running", nil - } -} - -type StateGenerator struct { - position int - stateSequence []string -} - -func (r *StateGenerator) NextState() (int, string, error) { - p, v := r.position, "" - if len(r.stateSequence)-1 >= p { - v = r.stateSequence[p] - } else { - return -1, "", errors.New("No more states available") - } - - r.position += 1 - - return p, v, nil -} - -func NewStateGenerator(sequence []string) *StateGenerator { - r := &StateGenerator{} - r.stateSequence = sequence - - return r -} - -func InconsistentStateRefreshFunc() StateRefreshFunc { - sequence := []string{ - "done", "replicating", - "done", "done", "done", - "replicating", - "done", "done", "done", - } - - r := NewStateGenerator(sequence) - - return func() (interface{}, string, error) { - idx, s, err := r.NextState() - if err != nil { - return nil, "", err - } - - return idx, s, nil - } -} - -func UnknownPendingStateRefreshFunc() StateRefreshFunc { - sequence := []string{ - "unknown1", "unknown2", "done", - } - - r := NewStateGenerator(sequence) - - return func() (interface{}, string, error) { - idx, s, err := r.NextState() - if err != nil { - return nil, "", err - } - - return idx, s, nil - } -} - -func TestWaitForState_inconsistent_positive(t *testing.T) { - conf := &StateChangeConf{ - Pending: []string{"replicating"}, - Target: []string{"done"}, - Refresh: InconsistentStateRefreshFunc(), - Timeout: 90 * time.Millisecond, - PollInterval: 10 * time.Millisecond, - ContinuousTargetOccurence: 3, - } - - idx, err := conf.WaitForState() - - if err != nil { - t.Fatalf("err: %s", err) - } - - if idx != 4 { - t.Fatalf("Expected index 4, given %d", idx.(int)) - } -} - -func TestWaitForState_inconsistent_negative(t *testing.T) { - refreshCount := int64(0) - f := InconsistentStateRefreshFunc() - refresh := func() (interface{}, string, error) { - atomic.AddInt64(&refreshCount, 1) - return f() - } - - conf := &StateChangeConf{ - Pending: []string{"replicating"}, - Target: []string{"done"}, - Refresh: refresh, - Timeout: 85 * time.Millisecond, - PollInterval: 10 * time.Millisecond, - ContinuousTargetOccurence: 4, - } - - _, err := conf.WaitForState() - - if err == nil { - t.Fatal("Expected timeout error. No error returned.") - } - - // we can't guarantee the exact number of refresh calls in the tests by - // timing them, but we want to make sure the test at least went through th - // required states. - if atomic.LoadInt64(&refreshCount) < 6 { - t.Fatal("refreshed called too few times") - } - - expectedErr := "timeout while waiting for state to become 'done'" - if !strings.HasPrefix(err.Error(), expectedErr) { - t.Fatalf("error prefix doesn't match.\nExpected: %q\nGiven: %q\n", expectedErr, err.Error()) - } -} - -func TestWaitForState_timeout(t *testing.T) { - old := refreshGracePeriod - refreshGracePeriod = 5 * time.Millisecond - defer func() { - refreshGracePeriod = old - }() - - conf := &StateChangeConf{ - Pending: []string{"pending", "incomplete"}, - Target: []string{"running"}, - Refresh: TimeoutStateRefreshFunc(), - Timeout: 1 * time.Millisecond, - } - - obj, err := conf.WaitForState() - - if err == nil { - t.Fatal("Expected timeout error. No error returned.") - } - - expectedErr := "timeout while waiting for state to become 'running' (timeout: 1ms)" - if err.Error() != expectedErr { - t.Fatalf("Errors don't match.\nExpected: %q\nGiven: %q\n", expectedErr, err.Error()) - } - - if obj != nil { - t.Fatalf("should not return obj") - } -} - -// Make sure a timeout actually cancels the refresh goroutine and waits for its -// return. -func TestWaitForState_cancel(t *testing.T) { - // make this refresh func block until we cancel it - cancel := make(chan struct{}) - refresh := func() (interface{}, string, error) { - <-cancel - return nil, "pending", nil - } - conf := &StateChangeConf{ - Pending: []string{"pending", "incomplete"}, - Target: []string{"running"}, - Refresh: refresh, - Timeout: 10 * time.Millisecond, - PollInterval: 10 * time.Second, - } - - var obj interface{} - var err error - - waitDone := make(chan struct{}) - go func() { - defer close(waitDone) - obj, err = conf.WaitForState() - }() - - // make sure WaitForState is blocked - select { - case <-waitDone: - t.Fatal("WaitForState returned too early") - case <-time.After(10 * time.Millisecond): - } - - // unlock the refresh function - close(cancel) - // make sure WaitForState returns - select { - case <-waitDone: - case <-time.After(time.Second): - t.Fatal("WaitForState didn't return after refresh finished") - } - - if err == nil { - t.Fatal("Expected timeout error. No error returned.") - } - - expectedErr := "timeout while waiting for state to become 'running'" - if !strings.HasPrefix(err.Error(), expectedErr) { - t.Fatalf("Errors don't match.\nExpected: %q\nGiven: %q\n", expectedErr, err.Error()) - } - - if obj != nil { - t.Fatalf("should not return obj") - } - -} - -func TestWaitForState_success(t *testing.T) { - conf := &StateChangeConf{ - Pending: []string{"pending", "incomplete"}, - Target: []string{"running"}, - Refresh: SuccessfulStateRefreshFunc(), - Timeout: 200 * time.Second, - } - - obj, err := conf.WaitForState() - if err != nil { - t.Fatalf("err: %s", err) - } - if obj == nil { - t.Fatalf("should return obj") - } -} - -func TestWaitForState_successUnknownPending(t *testing.T) { - conf := &StateChangeConf{ - Target: []string{"done"}, - Refresh: UnknownPendingStateRefreshFunc(), - Timeout: 200 * time.Second, - } - - obj, err := conf.WaitForState() - if err != nil { - t.Fatalf("err: %s", err) - } - if obj == nil { - t.Fatalf("should return obj") - } -} - -func TestWaitForState_successEmpty(t *testing.T) { - conf := &StateChangeConf{ - Pending: []string{"pending", "incomplete"}, - Target: []string{}, - Refresh: func() (interface{}, string, error) { - return nil, "", nil - }, - Timeout: 200 * time.Second, - } - - obj, err := conf.WaitForState() - if err != nil { - t.Fatalf("err: %s", err) - } - if obj != nil { - t.Fatalf("obj should be nil") - } -} - -func TestWaitForState_failureEmpty(t *testing.T) { - conf := &StateChangeConf{ - Pending: []string{"pending", "incomplete"}, - Target: []string{}, - NotFoundChecks: 1, - Refresh: func() (interface{}, string, error) { - return 42, "pending", nil - }, - PollInterval: 10 * time.Millisecond, - Timeout: 100 * time.Millisecond, - } - - _, err := conf.WaitForState() - if err == nil { - t.Fatal("Expected timeout error. Got none.") - } - expectedErr := "timeout while waiting for resource to be gone (last state: 'pending', timeout: 100ms)" - if err.Error() != expectedErr { - t.Fatalf("Errors don't match.\nExpected: %q\nGiven: %q\n", expectedErr, err.Error()) - } -} - -func TestWaitForState_failure(t *testing.T) { - conf := &StateChangeConf{ - Pending: []string{"pending", "incomplete"}, - Target: []string{"running"}, - Refresh: FailedStateRefreshFunc(), - Timeout: 200 * time.Second, - } - - obj, err := conf.WaitForState() - if err == nil { - t.Fatal("Expected error. No error returned.") - } - expectedErr := "failed" - if err.Error() != expectedErr { - t.Fatalf("Errors don't match.\nExpected: %q\nGiven: %q\n", expectedErr, err.Error()) - } - if obj != nil { - t.Fatalf("should not return obj") - } -} diff --git a/helper/resource/testing.go b/helper/resource/testing.go deleted file mode 100644 index c36ff4b2e..000000000 --- a/helper/resource/testing.go +++ /dev/null @@ -1,1285 +0,0 @@ -package resource - -import ( - "bytes" - "flag" - "fmt" - "io/ioutil" - "log" - "os" - "path/filepath" - "reflect" - "regexp" - "strings" - "testing" - - "github.com/davecgh/go-spew/spew" - "github.com/hashicorp/errwrap" - "github.com/hashicorp/go-multierror" - "github.com/mitchellh/colorstring" - - "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/command/format" - "github.com/hashicorp/terraform/configs" - "github.com/hashicorp/terraform/configs/configload" - "github.com/hashicorp/terraform/internal/initwd" - "github.com/hashicorp/terraform/providers" - "github.com/hashicorp/terraform/states" - "github.com/hashicorp/terraform/terraform" - "github.com/hashicorp/terraform/tfdiags" - - _ "github.com/hashicorp/terraform/internal/logging" -) - -// flagSweep is a flag available when running tests on the command line. It -// contains a comma seperated list of regions to for the sweeper functions to -// run in. This flag bypasses the normal Test path and instead runs functions designed to -// clean up any leaked resources a testing environment could have created. It is -// a best effort attempt, and relies on Provider authors to implement "Sweeper" -// methods for resources. - -// Adding Sweeper methods with AddTestSweepers will -// construct a list of sweeper funcs to be called here. We iterate through -// regions provided by the sweep flag, and for each region we iterate through the -// tests, and exit on any errors. At time of writing, sweepers are ran -// sequentially, however they can list dependencies to be ran first. We track -// the sweepers that have been ran, so as to not run a sweeper twice for a given -// region. -// -// WARNING: -// Sweepers are designed to be destructive. You should not use the -sweep flag -// in any environment that is not strictly a test environment. Resources will be -// destroyed. - -var flagSweep = flag.String("sweep", "", "List of Regions to run available Sweepers") -var flagSweepRun = flag.String("sweep-run", "", "Comma seperated list of Sweeper Tests to run") -var sweeperFuncs map[string]*Sweeper - -// map of sweepers that have ran, and the success/fail status based on any error -// raised -var sweeperRunList map[string]bool - -// type SweeperFunc is a signature for a function that acts as a sweeper. It -// accepts a string for the region that the sweeper is to be ran in. This -// function must be able to construct a valid client for that region. -type SweeperFunc func(r string) error - -type Sweeper struct { - // Name for sweeper. Must be unique to be ran by the Sweeper Runner - Name string - - // Dependencies list the const names of other Sweeper functions that must be ran - // prior to running this Sweeper. This is an ordered list that will be invoked - // recursively at the helper/resource level - Dependencies []string - - // Sweeper function that when invoked sweeps the Provider of specific - // resources - F SweeperFunc -} - -func init() { - sweeperFuncs = make(map[string]*Sweeper) -} - -// AddTestSweepers function adds a given name and Sweeper configuration -// pair to the internal sweeperFuncs map. Invoke this function to register a -// resource sweeper to be available for running when the -sweep flag is used -// with `go test`. Sweeper names must be unique to help ensure a given sweeper -// is only ran once per run. -func AddTestSweepers(name string, s *Sweeper) { - if _, ok := sweeperFuncs[name]; ok { - log.Fatalf("[ERR] Error adding (%s) to sweeperFuncs: function already exists in map", name) - } - - sweeperFuncs[name] = s -} - -func TestMain(m *testing.M) { - flag.Parse() - if *flagSweep != "" { - // parse flagSweep contents for regions to run - regions := strings.Split(*flagSweep, ",") - - // get filtered list of sweepers to run based on sweep-run flag - sweepers := filterSweepers(*flagSweepRun, sweeperFuncs) - for _, region := range regions { - region = strings.TrimSpace(region) - // reset sweeperRunList for each region - sweeperRunList = map[string]bool{} - - log.Printf("[DEBUG] Running Sweepers for region (%s):\n", region) - for _, sweeper := range sweepers { - if err := runSweeperWithRegion(region, sweeper); err != nil { - log.Fatalf("[ERR] error running (%s): %s", sweeper.Name, err) - } - } - - log.Printf("Sweeper Tests ran:\n") - for s, _ := range sweeperRunList { - fmt.Printf("\t- %s\n", s) - } - } - } else { - os.Exit(m.Run()) - } -} - -// filterSweepers takes a comma seperated string listing the names of sweepers -// to be ran, and returns a filtered set from the list of all of sweepers to -// run based on the names given. -func filterSweepers(f string, source map[string]*Sweeper) map[string]*Sweeper { - filterSlice := strings.Split(strings.ToLower(f), ",") - if len(filterSlice) == 1 && filterSlice[0] == "" { - // if the filter slice is a single element of "" then no sweeper list was - // given, so just return the full list - return source - } - - sweepers := make(map[string]*Sweeper) - for name, sweeper := range source { - for _, s := range filterSlice { - if strings.Contains(strings.ToLower(name), s) { - sweepers[name] = sweeper - } - } - } - return sweepers -} - -// runSweeperWithRegion recieves a sweeper and a region, and recursively calls -// itself with that region for every dependency found for that sweeper. If there -// are no dependencies, invoke the contained sweeper fun with the region, and -// add the success/fail status to the sweeperRunList. -func runSweeperWithRegion(region string, s *Sweeper) error { - for _, dep := range s.Dependencies { - if depSweeper, ok := sweeperFuncs[dep]; ok { - log.Printf("[DEBUG] Sweeper (%s) has dependency (%s), running..", s.Name, dep) - if err := runSweeperWithRegion(region, depSweeper); err != nil { - return err - } - } else { - log.Printf("[DEBUG] Sweeper (%s) has dependency (%s), but that sweeper was not found", s.Name, dep) - } - } - - if _, ok := sweeperRunList[s.Name]; ok { - log.Printf("[DEBUG] Sweeper (%s) already ran in region (%s)", s.Name, region) - return nil - } - - runE := s.F(region) - if runE == nil { - sweeperRunList[s.Name] = true - } else { - sweeperRunList[s.Name] = false - } - - return runE -} - -const TestEnvVar = "TF_ACC" - -// TestProvider can be implemented by any ResourceProvider to provide custom -// reset functionality at the start of an acceptance test. -// The helper/schema Provider implements this interface. -type TestProvider interface { - TestReset() error -} - -// TestCheckFunc is the callback type used with acceptance tests to check -// the state of a resource. The state passed in is the latest state known, -// or in the case of being after a destroy, it is the last known state when -// it was created. -type TestCheckFunc func(*terraform.State) error - -// ImportStateCheckFunc is the check function for ImportState tests -type ImportStateCheckFunc func([]*terraform.InstanceState) error - -// ImportStateIdFunc is an ID generation function to help with complex ID -// generation for ImportState tests. -type ImportStateIdFunc func(*terraform.State) (string, error) - -// TestCase is a single acceptance test case used to test the apply/destroy -// lifecycle of a resource in a specific configuration. -// -// When the destroy plan is executed, the config from the last TestStep -// is used to plan it. -type TestCase struct { - // IsUnitTest allows a test to run regardless of the TF_ACC - // environment variable. This should be used with care - only for - // fast tests on local resources (e.g. remote state with a local - // backend) but can be used to increase confidence in correct - // operation of Terraform without waiting for a full acctest run. - IsUnitTest bool - - // PreCheck, if non-nil, will be called before any test steps are - // executed. It will only be executed in the case that the steps - // would run, so it can be used for some validation before running - // acceptance tests, such as verifying that keys are setup. - PreCheck func() - - // Providers is the ResourceProvider that will be under test. - // - // Alternately, ProviderFactories can be specified for the providers - // that are valid. This takes priority over Providers. - // - // The end effect of each is the same: specifying the providers that - // are used within the tests. - Providers map[string]terraform.ResourceProvider - ProviderFactories map[string]terraform.ResourceProviderFactory - - // PreventPostDestroyRefresh can be set to true for cases where data sources - // are tested alongside real resources - PreventPostDestroyRefresh bool - - // CheckDestroy is called after the resource is finally destroyed - // to allow the tester to test that the resource is truly gone. - CheckDestroy TestCheckFunc - - // Steps are the apply sequences done within the context of the - // same state. Each step can have its own check to verify correctness. - Steps []TestStep - - // The settings below control the "ID-only refresh test." This is - // an enabled-by-default test that tests that a refresh can be - // refreshed with only an ID to result in the same attributes. - // This validates completeness of Refresh. - // - // IDRefreshName is the name of the resource to check. This will - // default to the first non-nil primary resource in the state. - // - // IDRefreshIgnore is a list of configuration keys that will be ignored. - IDRefreshName string - IDRefreshIgnore []string -} - -// TestStep is a single apply sequence of a test, done within the -// context of a state. -// -// Multiple TestSteps can be sequenced in a Test to allow testing -// potentially complex update logic. In general, simply create/destroy -// tests will only need one step. -type TestStep struct { - // ResourceName should be set to the name of the resource - // that is being tested. Example: "aws_instance.foo". Various test - // modes use this to auto-detect state information. - // - // This is only required if the test mode settings below say it is - // for the mode you're using. - ResourceName string - - // PreConfig is called before the Config is applied to perform any per-step - // setup that needs to happen. This is called regardless of "test mode" - // below. - PreConfig func() - - // Taint is a list of resource addresses to taint prior to the execution of - // the step. Be sure to only include this at a step where the referenced - // address will be present in state, as it will fail the test if the resource - // is missing. - // - // This option is ignored on ImportState tests, and currently only works for - // resources in the root module path. - Taint []string - - //--------------------------------------------------------------- - // Test modes. One of the following groups of settings must be - // set to determine what the test step will do. Ideally we would've - // used Go interfaces here but there are now hundreds of tests we don't - // want to re-type so instead we just determine which step logic - // to run based on what settings below are set. - //--------------------------------------------------------------- - - //--------------------------------------------------------------- - // Plan, Apply testing - //--------------------------------------------------------------- - - // Config a string of the configuration to give to Terraform. If this - // is set, then the TestCase will execute this step with the same logic - // as a `terraform apply`. - Config string - - // Check is called after the Config is applied. Use this step to - // make your own API calls to check the status of things, and to - // inspect the format of the ResourceState itself. - // - // If an error is returned, the test will fail. In this case, a - // destroy plan will still be attempted. - // - // If this is nil, no check is done on this step. - Check TestCheckFunc - - // Destroy will create a destroy plan if set to true. - Destroy bool - - // ExpectNonEmptyPlan can be set to true for specific types of tests that are - // looking to verify that a diff occurs - ExpectNonEmptyPlan bool - - // ExpectError allows the construction of test cases that we expect to fail - // with an error. The specified regexp must match against the error for the - // test to pass. - ExpectError *regexp.Regexp - - // PlanOnly can be set to only run `plan` with this configuration, and not - // actually apply it. This is useful for ensuring config changes result in - // no-op plans - PlanOnly bool - - // PreventDiskCleanup can be set to true for testing terraform modules which - // require access to disk at runtime. Note that this will leave files in the - // temp folder - PreventDiskCleanup bool - - // PreventPostDestroyRefresh can be set to true for cases where data sources - // are tested alongside real resources - PreventPostDestroyRefresh bool - - // SkipFunc is called before applying config, but after PreConfig - // This is useful for defining test steps with platform-dependent checks - SkipFunc func() (bool, error) - - //--------------------------------------------------------------- - // ImportState testing - //--------------------------------------------------------------- - - // ImportState, if true, will test the functionality of ImportState - // by importing the resource with ResourceName (must be set) and the - // ID of that resource. - ImportState bool - - // ImportStateId is the ID to perform an ImportState operation with. - // This is optional. If it isn't set, then the resource ID is automatically - // determined by inspecting the state for ResourceName's ID. - ImportStateId string - - // ImportStateIdPrefix is the prefix added in front of ImportStateId. - // This can be useful in complex import cases, where more than one - // attribute needs to be passed on as the Import ID. Mainly in cases - // where the ID is not known, and a known prefix needs to be added to - // the unset ImportStateId field. - ImportStateIdPrefix string - - // ImportStateIdFunc is a function that can be used to dynamically generate - // the ID for the ImportState tests. It is sent the state, which can be - // checked to derive the attributes necessary and generate the string in the - // desired format. - ImportStateIdFunc ImportStateIdFunc - - // ImportStateCheck checks the results of ImportState. It should be - // used to verify that the resulting value of ImportState has the - // proper resources, IDs, and attributes. - ImportStateCheck ImportStateCheckFunc - - // ImportStateVerify, if true, will also check that the state values - // that are finally put into the state after import match for all the - // IDs returned by the Import. Note that this checks for strict equality - // and does not respect DiffSuppressFunc or CustomizeDiff. - // - // ImportStateVerifyIgnore is a list of prefixes of fields that should - // not be verified to be equal. These can be set to ephemeral fields or - // fields that can't be refreshed and don't matter. - ImportStateVerify bool - ImportStateVerifyIgnore []string - - // provider s is used internally to maintain a reference to the - // underlying providers during the tests - providers map[string]terraform.ResourceProvider -} - -// Set to a file mask in sprintf format where %s is test name -const EnvLogPathMask = "TF_LOG_PATH_MASK" - -// ParallelTest performs an acceptance test on a resource, allowing concurrency -// with other ParallelTest. -// -// Tests will fail if they do not properly handle conditions to allow multiple -// tests to occur against the same resource or service (e.g. random naming). -// All other requirements of the Test function also apply to this function. -func ParallelTest(t TestT, c TestCase) { - t.Parallel() - Test(t, c) -} - -// Test performs an acceptance test on a resource. -// -// Tests are not run unless an environmental variable "TF_ACC" is -// set to some non-empty value. This is to avoid test cases surprising -// a user by creating real resources. -// -// Tests will fail unless the verbose flag (`go test -v`, or explicitly -// the "-test.v" flag) is set. Because some acceptance tests take quite -// long, we require the verbose flag so users are able to see progress -// output. -func Test(t TestT, c TestCase) { - // We only run acceptance tests if an env var is set because they're - // slow and generally require some outside configuration. You can opt out - // of this with OverrideEnvVar on individual TestCases. - if os.Getenv(TestEnvVar) == "" && !c.IsUnitTest { - t.Skip(fmt.Sprintf( - "Acceptance tests skipped unless env '%s' set", - TestEnvVar)) - return - } - - // We require verbose mode so that the user knows what is going on. - if !testTesting && !testing.Verbose() && !c.IsUnitTest { - t.Fatal("Acceptance tests must be run with the -v flag on tests") - return - } - - // Run the PreCheck if we have it - if c.PreCheck != nil { - c.PreCheck() - } - - providerFactories, err := testProviderFactories(c) - if err != nil { - t.Fatal(err) - } - - // get instances of all providers, so we can use the individual - // resources to shim the state during the tests. - providers := make(map[string]terraform.ResourceProvider) - legacyProviderFactories, err := testProviderFactoriesLegacy(c) - if err != nil { - t.Fatal(err) - } - for name, pf := range legacyProviderFactories { - p, err := pf() - if err != nil { - t.Fatal(err) - } - providers[name] = p - } - - opts := terraform.ContextOpts{Providers: providerFactories} - - // A single state variable to track the lifecycle, starting with no state - var state *terraform.State - - // Go through each step and run it - var idRefreshCheck *terraform.ResourceState - idRefresh := c.IDRefreshName != "" - errored := false - for i, step := range c.Steps { - // insert the providers into the step so we can get the resources for - // shimming the state - step.providers = providers - - var err error - log.Printf("[DEBUG] Test: Executing step %d", i) - - if step.SkipFunc != nil { - skip, err := step.SkipFunc() - if err != nil { - t.Fatal(err) - } - if skip { - log.Printf("[WARN] Skipping step %d", i) - continue - } - } - - if step.Config == "" && !step.ImportState { - err = fmt.Errorf( - "unknown test mode for step. Please see TestStep docs\n\n%#v", - step) - } else { - if step.ImportState { - if step.Config == "" { - step.Config = testProviderConfig(c) - } - - // Can optionally set step.Config in addition to - // step.ImportState, to provide config for the import. - state, err = testStepImportState(opts, state, step) - } else { - state, err = testStepConfig(opts, state, step) - } - } - - // If we expected an error, but did not get one, fail - if err == nil && step.ExpectError != nil { - errored = true - t.Error(fmt.Sprintf( - "Step %d, no error received, but expected a match to:\n\n%s\n\n", - i, step.ExpectError)) - break - } - - // If there was an error, exit - if err != nil { - // Perhaps we expected an error? Check if it matches - if step.ExpectError != nil { - if !step.ExpectError.MatchString(err.Error()) { - errored = true - t.Error(fmt.Sprintf( - "Step %d, expected error:\n\n%s\n\nTo match:\n\n%s\n\n", - i, err, step.ExpectError)) - break - } - } else { - errored = true - t.Error(fmt.Sprintf("Step %d error: %s", i, detailedErrorMessage(err))) - break - } - } - - // If we've never checked an id-only refresh and our state isn't - // empty, find the first resource and test it. - if idRefresh && idRefreshCheck == nil && !state.Empty() { - // Find the first non-nil resource in the state - for _, m := range state.Modules { - if len(m.Resources) > 0 { - if v, ok := m.Resources[c.IDRefreshName]; ok { - idRefreshCheck = v - } - - break - } - } - - // If we have an instance to check for refreshes, do it - // immediately. We do it in the middle of another test - // because it shouldn't affect the overall state (refresh - // is read-only semantically) and we want to fail early if - // this fails. If refresh isn't read-only, then this will have - // caught a different bug. - if idRefreshCheck != nil { - log.Printf( - "[WARN] Test: Running ID-only refresh check on %s", - idRefreshCheck.Primary.ID) - if err := testIDOnlyRefresh(c, opts, step, idRefreshCheck); err != nil { - log.Printf("[ERROR] Test: ID-only test failed: %s", err) - t.Error(fmt.Sprintf( - "[ERROR] Test: ID-only test failed: %s", err)) - break - } - } - } - } - - // If we never checked an id-only refresh, it is a failure. - if idRefresh { - if !errored && len(c.Steps) > 0 && idRefreshCheck == nil { - t.Error("ID-only refresh check never ran.") - } - } - - // If we have a state, then run the destroy - if state != nil { - lastStep := c.Steps[len(c.Steps)-1] - destroyStep := TestStep{ - Config: lastStep.Config, - Check: c.CheckDestroy, - Destroy: true, - PreventDiskCleanup: lastStep.PreventDiskCleanup, - PreventPostDestroyRefresh: c.PreventPostDestroyRefresh, - providers: providers, - } - - log.Printf("[WARN] Test: Executing destroy step") - state, err := testStep(opts, state, destroyStep) - if err != nil { - t.Error(fmt.Sprintf( - "Error destroying resource! WARNING: Dangling resources\n"+ - "may exist. The full state and error is shown below.\n\n"+ - "Error: %s\n\nState: %s", - err, - state)) - } - } else { - log.Printf("[WARN] Skipping destroy test since there is no state.") - } -} - -// testProviderConfig takes the list of Providers in a TestCase and returns a -// config with only empty provider blocks. This is useful for Import, where no -// config is provided, but the providers must be defined. -func testProviderConfig(c TestCase) string { - var lines []string - for p := range c.Providers { - lines = append(lines, fmt.Sprintf("provider %q {}\n", p)) - } - - return strings.Join(lines, "") -} - -// testProviderFactoriesLegacy is like testProviderFactories but it returns -// providers implementing the legacy interface terraform.ResourceProvider, -// rather than the current providers.Interface. -// -// It also identifies all providers as legacy-style single names rather than -// full addresses, for compatibility with legacy code that doesn't understand -// FQNs. -func testProviderFactoriesLegacy(c TestCase) (map[string]terraform.ResourceProviderFactory, error) { - ctxProviders := make(map[string]terraform.ResourceProviderFactory) - for k, pf := range c.ProviderFactories { - ctxProviders[k] = pf - } - - // add any fixed providers - for k, p := range c.Providers { - ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p) - } - return ctxProviders, nil -} - -// testProviderFactories combines the fixed Providers and -// ResourceProviderFactory functions into a single map of -// ResourceProviderFactory functions. -func testProviderFactories(c TestCase) (map[addrs.Provider]providers.Factory, error) { - ctxProviders, err := testProviderFactoriesLegacy(c) - if err != nil { - return nil, err - } - - // We additionally wrap all of the factories as a GRPCTestProvider, which - // allows them to appear as a new-style providers.Interface, rather than - // the legacy terraform.ResourceProvider. - newProviders := make(map[addrs.Provider]providers.Factory) - for legacyName, pf := range ctxProviders { - factory := pf // must copy to ensure each closure sees its own value - newProviders[addrs.NewDefaultProvider(legacyName)] = func() (providers.Interface, error) { - p, err := factory() - if err != nil { - return nil, err - } - - // The provider is wrapped in a GRPCTestProvider so that it can be - // passed back to terraform core as a providers.Interface, rather - // than the legacy ResourceProvider. - return GRPCTestProvider(p), nil - } - } - - return newProviders, nil -} - -// UnitTest is a helper to force the acceptance testing harness to run in the -// normal unit test suite. This should only be used for resource that don't -// have any external dependencies. -func UnitTest(t TestT, c TestCase) { - c.IsUnitTest = true - Test(t, c) -} - -func testIDOnlyRefresh(c TestCase, opts terraform.ContextOpts, step TestStep, r *terraform.ResourceState) error { - // TODO: We guard by this right now so master doesn't explode. We - // need to remove this eventually to make this part of the normal tests. - if os.Getenv("TF_ACC_IDONLY") == "" { - return nil - } - - addr := addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: r.Type, - Name: "foo", - }.Instance(addrs.NoKey) - absAddr := addr.Absolute(addrs.RootModuleInstance) - - // Build the state. The state is just the resource with an ID. There - // are no attributes. We only set what is needed to perform a refresh. - state := states.NewState() - state.RootModule().SetResourceInstanceCurrent( - addr, - &states.ResourceInstanceObjectSrc{ - AttrsFlat: r.Primary.Attributes, - Status: states.ObjectReady, - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("placeholder"), - Module: addrs.RootModule, - }, - ) - - // Create the config module. We use the full config because Refresh - // doesn't have access to it and we may need things like provider - // configurations. The initial implementation of id-only checks used - // an empty config module, but that caused the aforementioned problems. - cfg, err := testConfig(opts, step) - if err != nil { - return err - } - - // Initialize the context - opts.Config = cfg - opts.State = state - ctx, ctxDiags := terraform.NewContext(&opts) - if ctxDiags.HasErrors() { - return ctxDiags.Err() - } - if diags := ctx.Validate(); len(diags) > 0 { - if diags.HasErrors() { - return errwrap.Wrapf("config is invalid: {{err}}", diags.Err()) - } - - log.Printf("[WARN] Config warnings:\n%s", diags.Err().Error()) - } - - // Refresh! - state, refreshDiags := ctx.Refresh() - if refreshDiags.HasErrors() { - return refreshDiags.Err() - } - - // Verify attribute equivalence. - actualR := state.ResourceInstance(absAddr) - if actualR == nil { - return fmt.Errorf("Resource gone!") - } - if actualR.Current == nil { - return fmt.Errorf("Resource has no primary instance") - } - actual := actualR.Current.AttrsFlat - expected := r.Primary.Attributes - // Remove fields we're ignoring - for _, v := range c.IDRefreshIgnore { - for k, _ := range actual { - if strings.HasPrefix(k, v) { - delete(actual, k) - } - } - for k, _ := range expected { - if strings.HasPrefix(k, v) { - delete(expected, k) - } - } - } - - if !reflect.DeepEqual(actual, expected) { - // Determine only the different attributes - for k, v := range expected { - if av, ok := actual[k]; ok && v == av { - delete(expected, k) - delete(actual, k) - } - } - - spewConf := spew.NewDefaultConfig() - spewConf.SortKeys = true - return fmt.Errorf( - "Attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+ - "\n\n%s\n\n%s", - spewConf.Sdump(actual), spewConf.Sdump(expected)) - } - - return nil -} - -func testConfig(opts terraform.ContextOpts, step TestStep) (*configs.Config, error) { - if step.PreConfig != nil { - step.PreConfig() - } - - cfgPath, err := ioutil.TempDir("", "tf-test") - if err != nil { - return nil, fmt.Errorf("Error creating temporary directory for config: %s", err) - } - - if step.PreventDiskCleanup { - log.Printf("[INFO] Skipping defer os.RemoveAll call") - } else { - defer os.RemoveAll(cfgPath) - } - - // Write the main configuration file - err = ioutil.WriteFile(filepath.Join(cfgPath, "main.tf"), []byte(step.Config), os.ModePerm) - if err != nil { - return nil, fmt.Errorf("Error creating temporary file for config: %s", err) - } - - // Create directory for our child modules, if any. - modulesDir := filepath.Join(cfgPath, ".modules") - err = os.Mkdir(modulesDir, os.ModePerm) - if err != nil { - return nil, fmt.Errorf("Error creating child modules directory: %s", err) - } - - inst := initwd.NewModuleInstaller(modulesDir, nil) - _, installDiags := inst.InstallModules(cfgPath, true, initwd.ModuleInstallHooksImpl{}) - if installDiags.HasErrors() { - return nil, installDiags.Err() - } - - loader, err := configload.NewLoader(&configload.Config{ - ModulesDir: modulesDir, - }) - if err != nil { - return nil, fmt.Errorf("failed to create config loader: %s", err) - } - - config, configDiags := loader.LoadConfig(cfgPath) - if configDiags.HasErrors() { - return nil, configDiags - } - - return config, nil -} - -func testResource(c TestStep, state *terraform.State) (*terraform.ResourceState, error) { - if c.ResourceName == "" { - return nil, fmt.Errorf("ResourceName must be set in TestStep") - } - - for _, m := range state.Modules { - if len(m.Resources) > 0 { - if v, ok := m.Resources[c.ResourceName]; ok { - return v, nil - } - } - } - - return nil, fmt.Errorf( - "Resource specified by ResourceName couldn't be found: %s", c.ResourceName) -} - -// ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into -// a single TestCheckFunc. -// -// As a user testing their provider, this lets you decompose your checks -// into smaller pieces more easily. -func ComposeTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { - return func(s *terraform.State) error { - for i, f := range fs { - if err := f(s); err != nil { - return fmt.Errorf("Check %d/%d error: %s", i+1, len(fs), err) - } - } - - return nil - } -} - -// ComposeAggregateTestCheckFunc lets you compose multiple TestCheckFuncs into -// a single TestCheckFunc. -// -// As a user testing their provider, this lets you decompose your checks -// into smaller pieces more easily. -// -// Unlike ComposeTestCheckFunc, ComposeAggergateTestCheckFunc runs _all_ of the -// TestCheckFuncs and aggregates failures. -func ComposeAggregateTestCheckFunc(fs ...TestCheckFunc) TestCheckFunc { - return func(s *terraform.State) error { - var result *multierror.Error - - for i, f := range fs { - if err := f(s); err != nil { - result = multierror.Append(result, fmt.Errorf("Check %d/%d error: %s", i+1, len(fs), err)) - } - } - - return result.ErrorOrNil() - } -} - -// TestCheckResourceAttrSet is a TestCheckFunc which ensures a value -// exists in state for the given name/key combination. It is useful when -// testing that computed values were set, when it is not possible to -// know ahead of time what the values will be. -func TestCheckResourceAttrSet(name, key string) TestCheckFunc { - return func(s *terraform.State) error { - is, err := primaryInstanceState(s, name) - if err != nil { - return err - } - - return testCheckResourceAttrSet(is, name, key) - } -} - -// TestCheckModuleResourceAttrSet - as per TestCheckResourceAttrSet but with -// support for non-root modules -func TestCheckModuleResourceAttrSet(mp []string, name string, key string) TestCheckFunc { - mpt := addrs.Module(mp).UnkeyedInstanceShim() - return func(s *terraform.State) error { - is, err := modulePathPrimaryInstanceState(s, mpt, name) - if err != nil { - return err - } - - return testCheckResourceAttrSet(is, name, key) - } -} - -func testCheckResourceAttrSet(is *terraform.InstanceState, name string, key string) error { - if val, ok := is.Attributes[key]; !ok || val == "" { - return fmt.Errorf("%s: Attribute '%s' expected to be set", name, key) - } - - return nil -} - -// TestCheckResourceAttr is a TestCheckFunc which validates -// the value in state for the given name/key combination. -func TestCheckResourceAttr(name, key, value string) TestCheckFunc { - return func(s *terraform.State) error { - is, err := primaryInstanceState(s, name) - if err != nil { - return err - } - - return testCheckResourceAttr(is, name, key, value) - } -} - -// TestCheckModuleResourceAttr - as per TestCheckResourceAttr but with -// support for non-root modules -func TestCheckModuleResourceAttr(mp []string, name string, key string, value string) TestCheckFunc { - mpt := addrs.Module(mp).UnkeyedInstanceShim() - return func(s *terraform.State) error { - is, err := modulePathPrimaryInstanceState(s, mpt, name) - if err != nil { - return err - } - - return testCheckResourceAttr(is, name, key, value) - } -} - -func testCheckResourceAttr(is *terraform.InstanceState, name string, key string, value string) error { - // Empty containers may be elided from the state. - // If the intent here is to check for an empty container, allow the key to - // also be non-existent. - emptyCheck := false - if value == "0" && (strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%")) { - emptyCheck = true - } - - if v, ok := is.Attributes[key]; !ok || v != value { - if emptyCheck && !ok { - return nil - } - - if !ok { - return fmt.Errorf("%s: Attribute '%s' not found", name, key) - } - - return fmt.Errorf( - "%s: Attribute '%s' expected %#v, got %#v", - name, - key, - value, - v) - } - return nil -} - -// TestCheckNoResourceAttr is a TestCheckFunc which ensures that -// NO value exists in state for the given name/key combination. -func TestCheckNoResourceAttr(name, key string) TestCheckFunc { - return func(s *terraform.State) error { - is, err := primaryInstanceState(s, name) - if err != nil { - return err - } - - return testCheckNoResourceAttr(is, name, key) - } -} - -// TestCheckModuleNoResourceAttr - as per TestCheckNoResourceAttr but with -// support for non-root modules -func TestCheckModuleNoResourceAttr(mp []string, name string, key string) TestCheckFunc { - mpt := addrs.Module(mp).UnkeyedInstanceShim() - return func(s *terraform.State) error { - is, err := modulePathPrimaryInstanceState(s, mpt, name) - if err != nil { - return err - } - - return testCheckNoResourceAttr(is, name, key) - } -} - -func testCheckNoResourceAttr(is *terraform.InstanceState, name string, key string) error { - // Empty containers may sometimes be included in the state. - // If the intent here is to check for an empty container, allow the value to - // also be "0". - emptyCheck := false - if strings.HasSuffix(key, ".#") || strings.HasSuffix(key, ".%") { - emptyCheck = true - } - - val, exists := is.Attributes[key] - if emptyCheck && val == "0" { - return nil - } - - if exists { - return fmt.Errorf("%s: Attribute '%s' found when not expected", name, key) - } - - return nil -} - -// TestMatchResourceAttr is a TestCheckFunc which checks that the value -// in state for the given name/key combination matches the given regex. -func TestMatchResourceAttr(name, key string, r *regexp.Regexp) TestCheckFunc { - return func(s *terraform.State) error { - is, err := primaryInstanceState(s, name) - if err != nil { - return err - } - - return testMatchResourceAttr(is, name, key, r) - } -} - -// TestModuleMatchResourceAttr - as per TestMatchResourceAttr but with -// support for non-root modules -func TestModuleMatchResourceAttr(mp []string, name string, key string, r *regexp.Regexp) TestCheckFunc { - mpt := addrs.Module(mp).UnkeyedInstanceShim() - return func(s *terraform.State) error { - is, err := modulePathPrimaryInstanceState(s, mpt, name) - if err != nil { - return err - } - - return testMatchResourceAttr(is, name, key, r) - } -} - -func testMatchResourceAttr(is *terraform.InstanceState, name string, key string, r *regexp.Regexp) error { - if !r.MatchString(is.Attributes[key]) { - return fmt.Errorf( - "%s: Attribute '%s' didn't match %q, got %#v", - name, - key, - r.String(), - is.Attributes[key]) - } - - return nil -} - -// TestCheckResourceAttrPtr is like TestCheckResourceAttr except the -// value is a pointer so that it can be updated while the test is running. -// It will only be dereferenced at the point this step is run. -func TestCheckResourceAttrPtr(name string, key string, value *string) TestCheckFunc { - return func(s *terraform.State) error { - return TestCheckResourceAttr(name, key, *value)(s) - } -} - -// TestCheckModuleResourceAttrPtr - as per TestCheckResourceAttrPtr but with -// support for non-root modules -func TestCheckModuleResourceAttrPtr(mp []string, name string, key string, value *string) TestCheckFunc { - return func(s *terraform.State) error { - return TestCheckModuleResourceAttr(mp, name, key, *value)(s) - } -} - -// TestCheckResourceAttrPair is a TestCheckFunc which validates that the values -// in state for a pair of name/key combinations are equal. -func TestCheckResourceAttrPair(nameFirst, keyFirst, nameSecond, keySecond string) TestCheckFunc { - return func(s *terraform.State) error { - isFirst, err := primaryInstanceState(s, nameFirst) - if err != nil { - return err - } - - isSecond, err := primaryInstanceState(s, nameSecond) - if err != nil { - return err - } - - return testCheckResourceAttrPair(isFirst, nameFirst, keyFirst, isSecond, nameSecond, keySecond) - } -} - -// TestCheckModuleResourceAttrPair - as per TestCheckResourceAttrPair but with -// support for non-root modules -func TestCheckModuleResourceAttrPair(mpFirst []string, nameFirst string, keyFirst string, mpSecond []string, nameSecond string, keySecond string) TestCheckFunc { - mptFirst := addrs.Module(mpFirst).UnkeyedInstanceShim() - mptSecond := addrs.Module(mpSecond).UnkeyedInstanceShim() - return func(s *terraform.State) error { - isFirst, err := modulePathPrimaryInstanceState(s, mptFirst, nameFirst) - if err != nil { - return err - } - - isSecond, err := modulePathPrimaryInstanceState(s, mptSecond, nameSecond) - if err != nil { - return err - } - - return testCheckResourceAttrPair(isFirst, nameFirst, keyFirst, isSecond, nameSecond, keySecond) - } -} - -func testCheckResourceAttrPair(isFirst *terraform.InstanceState, nameFirst string, keyFirst string, isSecond *terraform.InstanceState, nameSecond string, keySecond string) error { - vFirst, okFirst := isFirst.Attributes[keyFirst] - vSecond, okSecond := isSecond.Attributes[keySecond] - - // Container count values of 0 should not be relied upon, and not reliably - // maintained by helper/schema. For the purpose of tests, consider unset and - // 0 to be equal. - if len(keyFirst) > 2 && len(keySecond) > 2 && keyFirst[len(keyFirst)-2:] == keySecond[len(keySecond)-2:] && - (strings.HasSuffix(keyFirst, ".#") || strings.HasSuffix(keyFirst, ".%")) { - // they have the same suffix, and it is a collection count key. - if vFirst == "0" || vFirst == "" { - okFirst = false - } - if vSecond == "0" || vSecond == "" { - okSecond = false - } - } - - if okFirst != okSecond { - if !okFirst { - return fmt.Errorf("%s: Attribute %q not set, but %q is set in %s as %q", nameFirst, keyFirst, keySecond, nameSecond, vSecond) - } - return fmt.Errorf("%s: Attribute %q is %q, but %q is not set in %s", nameFirst, keyFirst, vFirst, keySecond, nameSecond) - } - if !(okFirst || okSecond) { - // If they both don't exist then they are equally unset, so that's okay. - return nil - } - - if vFirst != vSecond { - return fmt.Errorf( - "%s: Attribute '%s' expected %#v, got %#v", - nameFirst, - keyFirst, - vSecond, - vFirst) - } - - return nil -} - -// TestCheckOutput checks an output in the Terraform configuration -func TestCheckOutput(name, value string) TestCheckFunc { - return func(s *terraform.State) error { - ms := s.RootModule() - rs, ok := ms.Outputs[name] - if !ok { - return fmt.Errorf("Not found: %s", name) - } - - if rs.Value != value { - return fmt.Errorf( - "Output '%s': expected %#v, got %#v", - name, - value, - rs) - } - - return nil - } -} - -func TestMatchOutput(name string, r *regexp.Regexp) TestCheckFunc { - return func(s *terraform.State) error { - ms := s.RootModule() - rs, ok := ms.Outputs[name] - if !ok { - return fmt.Errorf("Not found: %s", name) - } - - if !r.MatchString(rs.Value.(string)) { - return fmt.Errorf( - "Output '%s': %#v didn't match %q", - name, - rs, - r.String()) - } - - return nil - } -} - -// TestT is the interface used to handle the test lifecycle of a test. -// -// Users should just use a *testing.T object, which implements this. -type TestT interface { - Error(args ...interface{}) - Fatal(args ...interface{}) - Skip(args ...interface{}) - Name() string - Parallel() -} - -// This is set to true by unit tests to alter some behavior -var testTesting = false - -// modulePrimaryInstanceState returns the instance state for the given resource -// name in a ModuleState -func modulePrimaryInstanceState(s *terraform.State, ms *terraform.ModuleState, name string) (*terraform.InstanceState, error) { - rs, ok := ms.Resources[name] - if !ok { - return nil, fmt.Errorf("Not found: %s in %s", name, ms.Path) - } - - is := rs.Primary - if is == nil { - return nil, fmt.Errorf("No primary instance: %s in %s", name, ms.Path) - } - - return is, nil -} - -// modulePathPrimaryInstanceState returns the primary instance state for the -// given resource name in a given module path. -func modulePathPrimaryInstanceState(s *terraform.State, mp addrs.ModuleInstance, name string) (*terraform.InstanceState, error) { - ms := s.ModuleByPath(mp) - if ms == nil { - return nil, fmt.Errorf("No module found at: %s", mp) - } - - return modulePrimaryInstanceState(s, ms, name) -} - -// primaryInstanceState returns the primary instance state for the given -// resource name in the root module. -func primaryInstanceState(s *terraform.State, name string) (*terraform.InstanceState, error) { - ms := s.RootModule() - return modulePrimaryInstanceState(s, ms, name) -} - -// operationError is a specialized implementation of error used to describe -// failures during one of the several operations performed for a particular -// test case. -type operationError struct { - OpName string - Diags tfdiags.Diagnostics -} - -func newOperationError(opName string, diags tfdiags.Diagnostics) error { - return operationError{opName, diags} -} - -// Error returns a terse error string containing just the basic diagnostic -// messages, for situations where normal Go error behavior is appropriate. -func (err operationError) Error() string { - return fmt.Sprintf("errors during %s: %s", err.OpName, err.Diags.Err().Error()) -} - -// ErrorDetail is like Error except it includes verbosely-rendered diagnostics -// similar to what would come from a normal Terraform run, which include -// additional context not included in Error(). -func (err operationError) ErrorDetail() string { - var buf bytes.Buffer - fmt.Fprintf(&buf, "errors during %s:", err.OpName) - clr := &colorstring.Colorize{Disable: true, Colors: colorstring.DefaultColors} - for _, diag := range err.Diags { - diagStr := format.Diagnostic(diag, nil, clr, 78) - buf.WriteByte('\n') - buf.WriteString(diagStr) - } - return buf.String() -} - -// detailedErrorMessage is a helper for calling ErrorDetail on an error if -// it is an operationError or just taking Error otherwise. -func detailedErrorMessage(err error) string { - switch tErr := err.(type) { - case operationError: - return tErr.ErrorDetail() - default: - return err.Error() - } -} diff --git a/helper/resource/testing_config.go b/helper/resource/testing_config.go deleted file mode 100644 index 74739c8a0..000000000 --- a/helper/resource/testing_config.go +++ /dev/null @@ -1,378 +0,0 @@ -package resource - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "log" - "sort" - "strings" - - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/states" - - "github.com/hashicorp/errwrap" - "github.com/hashicorp/terraform/plans" - "github.com/hashicorp/terraform/terraform" - "github.com/hashicorp/terraform/tfdiags" -) - -// testStepConfig runs a config-mode test step -func testStepConfig( - opts terraform.ContextOpts, - state *terraform.State, - step TestStep) (*terraform.State, error) { - return testStep(opts, state, step) -} - -func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { - if !step.Destroy { - if err := testStepTaint(state, step); err != nil { - return state, err - } - } - - cfg, err := testConfig(opts, step) - if err != nil { - return state, err - } - - var stepDiags tfdiags.Diagnostics - - // Build the context - opts.Config = cfg - opts.State, err = shimLegacyState(state) - if err != nil { - return nil, err - } - - opts.Destroy = step.Destroy - ctx, stepDiags := terraform.NewContext(&opts) - if stepDiags.HasErrors() { - return state, fmt.Errorf("Error initializing context: %s", stepDiags.Err()) - } - if stepDiags := ctx.Validate(); len(stepDiags) > 0 { - if stepDiags.HasErrors() { - return state, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err()) - } - - log.Printf("[WARN] Config warnings:\n%s", stepDiags) - } - - // If this step is a PlanOnly step, skip over this first Plan and subsequent - // Apply, and use the follow up Plan that checks for perpetual diffs - if !step.PlanOnly { - // Plan! - p, stepDiags := ctx.Plan() - if stepDiags.HasErrors() { - return state, newOperationError("plan", stepDiags) - } - - newState := p.State - log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes)) - - // We need to keep a copy of the state prior to destroying - // such that destroy steps can verify their behavior in the check - // function - stateBeforeApplication := state.DeepCopy() - - // Apply the diff, creating real resources. - newState, stepDiags = ctx.Apply() - // shim the state first so the test can check the state on errors - state, err = shimNewState(newState, step.providers) - if err != nil { - return nil, err - } - if stepDiags.HasErrors() { - return state, newOperationError("apply", stepDiags) - } - - // Run any configured checks - if step.Check != nil { - if step.Destroy { - if err := step.Check(stateBeforeApplication); err != nil { - return state, fmt.Errorf("Check failed: %s", err) - } - } else { - if err := step.Check(state); err != nil { - return state, fmt.Errorf("Check failed: %s", err) - } - } - } - } - - // Now, verify that Plan is now empty and we don't have a perpetual diff issue - // We do this with TWO plans. One without a refresh. - p, stepDiags := ctx.Plan() - if stepDiags.HasErrors() { - return state, newOperationError("follow-up plan", stepDiags) - } - - // we don't technically need this any longer with plan handling refreshing, - // but run it anyway to ensure the context is working as expected. - p, stepDiags = ctx.Plan() - if stepDiags.HasErrors() { - return state, newOperationError("second follow-up plan", stepDiags) - } - empty := true - newState := p.State - - // the legacy tests never took outputs into account - for _, c := range p.Changes.Resources { - if c.Action != plans.NoOp { - empty = false - break - } - } - - if !empty { - if step.ExpectNonEmptyPlan { - log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes)) - } else { - return state, fmt.Errorf( - "After applying this step, the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes)) - } - } - - if !empty { - if step.ExpectNonEmptyPlan { - log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes)) - } else { - return state, fmt.Errorf( - "After applying this step and refreshing, "+ - "the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes)) - } - } - - // Made it here, but expected a non-empty plan, fail! - if step.ExpectNonEmptyPlan && empty { - return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!") - } - - // Made it here? Good job test step! - return state, nil -} - -// legacyPlanComparisonString produces a string representation of the changes -// from a plan and a given state togther, as was formerly produced by the -// String method of terraform.Plan. -// -// This is here only for compatibility with existing tests that predate our -// new plan and state types, and should not be used in new tests. Instead, use -// a library like "cmp" to do a deep equality and diff on the two -// data structures. -func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string { - return fmt.Sprintf( - "DIFF:\n\n%s\n\nSTATE:\n\n%s", - legacyDiffComparisonString(changes), - state.String(), - ) -} - -// legacyDiffComparisonString produces a string representation of the changes -// from a planned changes object, as was formerly produced by the String method -// of terraform.Diff. -// -// This is here only for compatibility with existing tests that predate our -// new plan types, and should not be used in new tests. Instead, use a library -// like "cmp" to do a deep equality check and diff on the two data structures. -func legacyDiffComparisonString(changes *plans.Changes) string { - // The old string representation of a plan was grouped by module, but - // our new plan structure is not grouped in that way and so we'll need - // to preprocess it in order to produce that grouping. - type ResourceChanges struct { - Current *plans.ResourceInstanceChangeSrc - Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc - } - byModule := map[string]map[string]*ResourceChanges{} - resourceKeys := map[string][]string{} - requiresReplace := map[string][]string{} - var moduleKeys []string - for _, rc := range changes.Resources { - if rc.Action == plans.NoOp { - // We won't mention no-op changes here at all, since the old plan - // model we are emulating here didn't have such a concept. - continue - } - moduleKey := rc.Addr.Module.String() - if _, exists := byModule[moduleKey]; !exists { - moduleKeys = append(moduleKeys, moduleKey) - byModule[moduleKey] = make(map[string]*ResourceChanges) - } - resourceKey := rc.Addr.Resource.String() - if _, exists := byModule[moduleKey][resourceKey]; !exists { - resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey) - byModule[moduleKey][resourceKey] = &ResourceChanges{ - Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc), - } - } - - if rc.DeposedKey == states.NotDeposed { - byModule[moduleKey][resourceKey].Current = rc - } else { - byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc - } - - rr := []string{} - for _, p := range rc.RequiredReplace.List() { - rr = append(rr, hcl2shim.FlatmapKeyFromPath(p)) - } - requiresReplace[resourceKey] = rr - } - sort.Strings(moduleKeys) - for _, ks := range resourceKeys { - sort.Strings(ks) - } - - var buf bytes.Buffer - - for _, moduleKey := range moduleKeys { - rcs := byModule[moduleKey] - var mBuf bytes.Buffer - - for _, resourceKey := range resourceKeys[moduleKey] { - rc := rcs[resourceKey] - - forceNewAttrs := requiresReplace[resourceKey] - - crud := "UPDATE" - if rc.Current != nil { - switch rc.Current.Action { - case plans.DeleteThenCreate: - crud = "DESTROY/CREATE" - case plans.CreateThenDelete: - crud = "CREATE/DESTROY" - case plans.Delete: - crud = "DESTROY" - case plans.Create: - crud = "CREATE" - } - } else { - // We must be working on a deposed object then, in which - // case destroying is the only possible action. - crud = "DESTROY" - } - - extra := "" - if rc.Current == nil && len(rc.Deposed) > 0 { - extra = " (deposed only)" - } - - fmt.Fprintf( - &mBuf, "%s: %s%s\n", - crud, resourceKey, extra, - ) - - attrNames := map[string]bool{} - var oldAttrs map[string]string - var newAttrs map[string]string - if rc.Current != nil { - if before := rc.Current.Before; before != nil { - ty, err := before.ImpliedType() - if err == nil { - val, err := before.Decode(ty) - if err == nil { - oldAttrs = hcl2shim.FlatmapValueFromHCL2(val) - for k := range oldAttrs { - attrNames[k] = true - } - } - } - } - if after := rc.Current.After; after != nil { - ty, err := after.ImpliedType() - if err == nil { - val, err := after.Decode(ty) - if err == nil { - newAttrs = hcl2shim.FlatmapValueFromHCL2(val) - for k := range newAttrs { - attrNames[k] = true - } - } - } - } - } - if oldAttrs == nil { - oldAttrs = make(map[string]string) - } - if newAttrs == nil { - newAttrs = make(map[string]string) - } - - attrNamesOrder := make([]string, 0, len(attrNames)) - keyLen := 0 - for n := range attrNames { - attrNamesOrder = append(attrNamesOrder, n) - if len(n) > keyLen { - keyLen = len(n) - } - } - sort.Strings(attrNamesOrder) - - for _, attrK := range attrNamesOrder { - v := newAttrs[attrK] - u := oldAttrs[attrK] - - if v == hcl2shim.UnknownVariableValue { - v = "" - } - // NOTE: we don't support here because we would - // need schema to do that. Excluding sensitive values - // is now done at the UI layer, and so should not be tested - // at the core layer. - - updateMsg := "" - - // This may not be as precise as in the old diff, as it matches - // everything under the attribute that was originally marked as - // ForceNew, but should help make it easier to determine what - // caused replacement here. - for _, k := range forceNewAttrs { - if strings.HasPrefix(attrK, k) { - updateMsg = " (forces new resource)" - break - } - } - - fmt.Fprintf( - &mBuf, " %s:%s %#v => %#v%s\n", - attrK, - strings.Repeat(" ", keyLen-len(attrK)), - u, v, - updateMsg, - ) - } - } - - if moduleKey == "" { // root module - buf.Write(mBuf.Bytes()) - buf.WriteByte('\n') - continue - } - - fmt.Fprintf(&buf, "%s:\n", moduleKey) - s := bufio.NewScanner(&mBuf) - for s.Scan() { - buf.WriteString(fmt.Sprintf(" %s\n", s.Text())) - } - } - - return buf.String() -} - -func testStepTaint(state *terraform.State, step TestStep) error { - for _, p := range step.Taint { - m := state.RootModule() - if m == nil { - return errors.New("no state") - } - rs, ok := m.Resources[p] - if !ok { - return fmt.Errorf("resource %q not found in state", p) - } - log.Printf("[WARN] Test: Explicitly tainting resource %q", p) - rs.Taint() - } - return nil -} diff --git a/helper/resource/testing_import_state.go b/helper/resource/testing_import_state.go deleted file mode 100644 index e163770e8..000000000 --- a/helper/resource/testing_import_state.go +++ /dev/null @@ -1,230 +0,0 @@ -package resource - -import ( - "fmt" - "log" - "reflect" - "strings" - - "github.com/davecgh/go-spew/spew" - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" - - "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/states" - "github.com/hashicorp/terraform/terraform" -) - -// testStepImportState runs an import state test step -func testStepImportState( - opts terraform.ContextOpts, - state *terraform.State, - step TestStep) (*terraform.State, error) { - - // Determine the ID to import - var importId string - switch { - case step.ImportStateIdFunc != nil: - var err error - importId, err = step.ImportStateIdFunc(state) - if err != nil { - return state, err - } - case step.ImportStateId != "": - importId = step.ImportStateId - default: - resource, err := testResource(step, state) - if err != nil { - return state, err - } - importId = resource.Primary.ID - } - - importPrefix := step.ImportStateIdPrefix - if importPrefix != "" { - importId = fmt.Sprintf("%s%s", importPrefix, importId) - } - - // Setup the context. We initialize with an empty state. We use the - // full config for provider configurations. - cfg, err := testConfig(opts, step) - if err != nil { - return state, err - } - - opts.Config = cfg - - // import tests start with empty state - opts.State = states.NewState() - - ctx, stepDiags := terraform.NewContext(&opts) - if stepDiags.HasErrors() { - return state, stepDiags.Err() - } - - // The test step provides the resource address as a string, so we need - // to parse it to get an addrs.AbsResourceAddress to pass in to the - // import method. - traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(step.ResourceName), "", hcl.Pos{}) - if hclDiags.HasErrors() { - return nil, hclDiags - } - importAddr, stepDiags := addrs.ParseAbsResourceInstance(traversal) - if stepDiags.HasErrors() { - return nil, stepDiags.Err() - } - - // Do the import - importedState, stepDiags := ctx.Import(&terraform.ImportOpts{ - Targets: []*terraform.ImportTarget{ - &terraform.ImportTarget{ - Addr: importAddr, - ID: importId, - }, - }, - }) - if stepDiags.HasErrors() { - log.Printf("[ERROR] Test: ImportState failure: %s", stepDiags.Err()) - return state, stepDiags.Err() - } - - newState, err := shimNewState(importedState, step.providers) - if err != nil { - return nil, err - } - // Go through the new state and verify - if step.ImportStateCheck != nil { - var states []*terraform.InstanceState - for _, r := range newState.RootModule().Resources { - if r.Primary != nil { - is := r.Primary.DeepCopy() - is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type - states = append(states, is) - } - } - if err := step.ImportStateCheck(states); err != nil { - return state, err - } - } - - // Verify that all the states match - if step.ImportStateVerify { - new := newState.RootModule().Resources - old := state.RootModule().Resources - for _, r := range new { - // Find the existing resource - var oldR *terraform.ResourceState - for _, r2 := range old { - if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type { - oldR = r2 - break - } - } - if oldR == nil { - return state, fmt.Errorf( - "Failed state verification, resource with ID %s not found", - r.Primary.ID) - } - - // We'll try our best to find the schema for this resource type - // so we can ignore Removed fields during validation. If we fail - // to find the schema then we won't ignore them and so the test - // will need to rely on explicit ImportStateVerifyIgnore, though - // this shouldn't happen in any reasonable case. - var rsrcSchema *schema.Resource - if providerAddr, diags := addrs.ParseAbsProviderConfigStr(r.Provider); !diags.HasErrors() { - // FIXME - providerType := providerAddr.Provider.Type - if provider, ok := step.providers[providerType]; ok { - if provider, ok := provider.(*schema.Provider); ok { - rsrcSchema = provider.ResourcesMap[r.Type] - } - } - } - - // don't add empty flatmapped containers, so we can more easily - // compare the attributes - skipEmpty := func(k, v string) bool { - if strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%") { - if v == "0" { - return true - } - } - return false - } - - // Compare their attributes - actual := make(map[string]string) - for k, v := range r.Primary.Attributes { - if skipEmpty(k, v) { - continue - } - actual[k] = v - } - - expected := make(map[string]string) - for k, v := range oldR.Primary.Attributes { - if skipEmpty(k, v) { - continue - } - expected[k] = v - } - - // Remove fields we're ignoring - for _, v := range step.ImportStateVerifyIgnore { - for k := range actual { - if strings.HasPrefix(k, v) { - delete(actual, k) - } - } - for k := range expected { - if strings.HasPrefix(k, v) { - delete(expected, k) - } - } - } - - // Also remove any attributes that are marked as "Removed" in the - // schema, if we have a schema to check that against. - if rsrcSchema != nil { - for k := range actual { - for _, schema := range rsrcSchema.SchemasForFlatmapPath(k) { - if schema.Removed != "" { - delete(actual, k) - break - } - } - } - for k := range expected { - for _, schema := range rsrcSchema.SchemasForFlatmapPath(k) { - if schema.Removed != "" { - delete(expected, k) - break - } - } - } - } - - if !reflect.DeepEqual(actual, expected) { - // Determine only the different attributes - for k, v := range expected { - if av, ok := actual[k]; ok && v == av { - delete(expected, k) - delete(actual, k) - } - } - - spewConf := spew.NewDefaultConfig() - spewConf.SortKeys = true - return state, fmt.Errorf( - "ImportStateVerify attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+ - "\n\n%s\n\n%s", - spewConf.Sdump(actual), spewConf.Sdump(expected)) - } - } - } - - // Return the old state (non-imported) so we don't change anything. - return state, nil -} diff --git a/helper/resource/testing_import_state_test.go b/helper/resource/testing_import_state_test.go deleted file mode 100644 index 9b2acc3c9..000000000 --- a/helper/resource/testing_import_state_test.go +++ /dev/null @@ -1,517 +0,0 @@ -package resource - -import ( - "errors" - "fmt" - "testing" - - "github.com/hashicorp/terraform/terraform" -) - -func TestTest_importState(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.ImportStateReturn = []*terraform.InstanceState{ - &terraform.InstanceState{ - ID: "foo", - Ephemeral: terraform.EphemeralState{Type: "test_instance"}, - }, - } - mp.RefreshFn = func( - i *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - return s, nil - } - - checked := false - checkFn := func(s []*terraform.InstanceState) error { - checked = true - - if s[0].ID != "foo" { - return fmt.Errorf("bad: %#v", s) - } - - return nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - - Steps: []TestStep{ - TestStep{ - Config: testConfigStrProvider, - ResourceName: "test_instance.foo", - ImportState: true, - ImportStateId: "foo", - ImportStateCheck: checkFn, - }, - }, - }) - - if mt.failed() { - t.Fatalf("test failed: %s", mt.failMessage()) - } - if !checked { - t.Fatal("didn't call check") - } -} - -func TestTest_importStateFail(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.ImportStateReturn = []*terraform.InstanceState{ - &terraform.InstanceState{ - ID: "bar", - Ephemeral: terraform.EphemeralState{Type: "test_instance"}, - }, - } - mp.RefreshFn = func( - i *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - return s, nil - } - - checked := false - checkFn := func(s []*terraform.InstanceState) error { - checked = true - - if s[0].ID != "foo" { - return fmt.Errorf("bad: %#v", s) - } - - return nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - - Steps: []TestStep{ - TestStep{ - Config: testConfigStrProvider, - ResourceName: "test_instance.foo", - ImportState: true, - ImportStateId: "foo", - ImportStateCheck: checkFn, - }, - }, - }) - - if !mt.failed() { - t.Fatal("should fail") - } - if !checked { - t.Fatal("didn't call check") - } -} - -func TestTest_importStateDetectId(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.DiffReturn = nil - mp.ApplyFn = func( - info *terraform.InstanceInfo, - state *terraform.InstanceState, - diff *terraform.InstanceDiff) (*terraform.InstanceState, error) { - if !diff.Destroy { - return &terraform.InstanceState{ - ID: "foo", - }, nil - } - - return nil, nil - } - - mp.RefreshFn = func( - i *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - return s, nil - } - - mp.ImportStateFn = func( - info *terraform.InstanceInfo, id string) ([]*terraform.InstanceState, error) { - if id != "foo" { - return nil, fmt.Errorf("bad import ID: %s", id) - } - - return []*terraform.InstanceState{ - &terraform.InstanceState{ - ID: "bar", - Ephemeral: terraform.EphemeralState{Type: "test_instance"}, - }, - }, nil - } - - checked := false - checkFn := func(s []*terraform.InstanceState) error { - checked = true - - if s[0].ID != "bar" { - return fmt.Errorf("bad: %#v", s) - } - - return nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - }, - TestStep{ - Config: testConfigStr, - ResourceName: "test_instance.foo", - ImportState: true, - ImportStateCheck: checkFn, - }, - }, - }) - - if mt.failed() { - t.Fatalf("test failed: %s", mt.failMessage()) - } - if !checked { - t.Fatal("didn't call check") - } -} - -func TestTest_importStateIdPrefix(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.DiffReturn = nil - mp.ApplyFn = func( - info *terraform.InstanceInfo, - state *terraform.InstanceState, - diff *terraform.InstanceDiff) (*terraform.InstanceState, error) { - if !diff.Destroy { - return &terraform.InstanceState{ - ID: "foo", - }, nil - } - - return nil, nil - } - - mp.RefreshFn = func( - i *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - return s, nil - } - - mp.ImportStateFn = func( - info *terraform.InstanceInfo, id string) ([]*terraform.InstanceState, error) { - if id != "bazfoo" { - return nil, fmt.Errorf("bad import ID: %s", id) - } - - return []*terraform.InstanceState{ - { - ID: "bar", - Ephemeral: terraform.EphemeralState{Type: "test_instance"}, - }, - }, nil - } - - checked := false - checkFn := func(s []*terraform.InstanceState) error { - checked = true - - if s[0].ID != "bar" { - return fmt.Errorf("bad: %#v", s) - } - - return nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - - Steps: []TestStep{ - { - Config: testConfigStr, - }, - { - Config: testConfigStr, - ResourceName: "test_instance.foo", - ImportState: true, - ImportStateCheck: checkFn, - ImportStateIdPrefix: "baz", - }, - }, - }) - - if mt.failed() { - t.Fatalf("test failed: %s", mt.failMessage()) - } - if !checked { - t.Fatal("didn't call check") - } -} - -func TestTest_importStateVerify(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.DiffReturn = nil - mp.ApplyFn = func( - info *terraform.InstanceInfo, - state *terraform.InstanceState, - diff *terraform.InstanceDiff) (*terraform.InstanceState, error) { - if !diff.Destroy { - return &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "bar", - }, - }, nil - } - - return nil, nil - } - - mp.RefreshFn = func( - i *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - if len(s.Attributes) == 0 { - s.Attributes = map[string]string{ - "id": s.ID, - "foo": "bar", - } - } - - return s, nil - } - - mp.ImportStateFn = func( - info *terraform.InstanceInfo, id string) ([]*terraform.InstanceState, error) { - if id != "foo" { - return nil, fmt.Errorf("bad import ID: %s", id) - } - - return []*terraform.InstanceState{ - &terraform.InstanceState{ - ID: "foo", - Ephemeral: terraform.EphemeralState{Type: "test_instance"}, - }, - }, nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - }, - TestStep{ - Config: testConfigStr, - ResourceName: "test_instance.foo", - ImportState: true, - ImportStateVerify: true, - }, - }, - }) - - if mt.failed() { - t.Fatalf("test failed: %s", mt.failMessage()) - } -} - -func TestTest_importStateVerifyFail(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.DiffReturn = nil - mp.ApplyFn = func( - info *terraform.InstanceInfo, - state *terraform.InstanceState, - diff *terraform.InstanceDiff) (*terraform.InstanceState, error) { - if !diff.Destroy { - return &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "bar", - }, - }, nil - } - - return nil, nil - } - - mp.RefreshFn = func( - i *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - return s, nil - } - - mp.ImportStateFn = func( - info *terraform.InstanceInfo, id string) ([]*terraform.InstanceState, error) { - if id != "foo" { - return nil, fmt.Errorf("bad import ID: %s", id) - } - - return []*terraform.InstanceState{ - &terraform.InstanceState{ - ID: "foo", - Ephemeral: terraform.EphemeralState{Type: "test_instance"}, - }, - }, nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - }, - TestStep{ - Config: testConfigStr, - ResourceName: "test_instance.foo", - ImportState: true, - ImportStateVerify: true, - }, - }, - }) - - if !mt.failed() { - t.Fatalf("test should fail") - } -} - -func TestTest_importStateIdFunc(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.ImportStateFn = func( - info *terraform.InstanceInfo, id string) ([]*terraform.InstanceState, error) { - if id != "foo:bar" { - return nil, fmt.Errorf("bad import ID: %s", id) - } - - return []*terraform.InstanceState{ - { - ID: "foo", - Ephemeral: terraform.EphemeralState{Type: "test_instance"}, - }, - }, nil - } - - mp.RefreshFn = func( - i *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - return s, nil - } - - checked := false - checkFn := func(s []*terraform.InstanceState) error { - checked = true - - if s[0].ID != "foo" { - return fmt.Errorf("bad: %#v", s) - } - - return nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - - Steps: []TestStep{ - TestStep{ - Config: testConfigStrProvider, - ResourceName: "test_instance.foo", - ImportState: true, - ImportStateIdFunc: func(*terraform.State) (string, error) { return "foo:bar", nil }, - ImportStateCheck: checkFn, - }, - }, - }) - - if mt.failed() { - t.Fatalf("test failed: %s", mt.failMessage()) - } - if !checked { - t.Fatal("didn't call check") - } -} - -func TestTest_importStateIdFuncFail(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.ImportStateFn = func( - info *terraform.InstanceInfo, id string) ([]*terraform.InstanceState, error) { - if id != "foo:bar" { - return nil, fmt.Errorf("bad import ID: %s", id) - } - - return []*terraform.InstanceState{ - { - ID: "foo", - Ephemeral: terraform.EphemeralState{Type: "test_instance"}, - }, - }, nil - } - - mp.RefreshFn = func( - i *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - return s, nil - } - - checkFn := func(s []*terraform.InstanceState) error { - if s[0].ID != "foo" { - return fmt.Errorf("bad: %#v", s) - } - - return nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - - Steps: []TestStep{ - TestStep{ - Config: testConfigStrProvider, - ResourceName: "test_instance.foo", - ImportState: true, - ImportStateIdFunc: func(*terraform.State) (string, error) { return "foo:bar", errors.New("foobar") }, - ImportStateCheck: checkFn, - }, - }, - }) - - if !mt.failed() { - t.Fatalf("test should fail") - } -} diff --git a/helper/resource/testing_test.go b/helper/resource/testing_test.go deleted file mode 100644 index 5d4891778..000000000 --- a/helper/resource/testing_test.go +++ /dev/null @@ -1,1361 +0,0 @@ -package resource - -import ( - "errors" - "flag" - "fmt" - "os" - "reflect" - "regexp" - "sort" - "strings" - "sync" - "sync/atomic" - "testing" - - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/terraform" -) - -func init() { - testTesting = true - - // TODO: Remove when we remove the guard on id checks - if err := os.Setenv("TF_ACC_IDONLY", "1"); err != nil { - panic(err) - } - - if err := os.Setenv(TestEnvVar, "1"); err != nil { - panic(err) - } -} - -// wrap the mock provider to implement TestProvider -type resetProvider struct { - *terraform.MockResourceProvider - mu sync.Mutex - TestResetCalled bool - TestResetError error -} - -func (p *resetProvider) TestReset() error { - p.mu.Lock() - defer p.mu.Unlock() - p.TestResetCalled = true - return p.TestResetError -} - -func TestParallelTest(t *testing.T) { - mt := new(mockT) - ParallelTest(mt, TestCase{}) - - if !mt.ParallelCalled { - t.Fatal("Parallel() not called") - } -} - -func TestTest(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := &resetProvider{ - MockResourceProvider: testProvider(), - } - - mp.DiffReturn = nil - - mp.ApplyFn = func( - info *terraform.InstanceInfo, - state *terraform.InstanceState, - diff *terraform.InstanceDiff) (*terraform.InstanceState, error) { - if !diff.Destroy { - return &terraform.InstanceState{ - ID: "foo", - }, nil - } - - return nil, nil - } - - var refreshCount int32 - mp.RefreshFn = func(*terraform.InstanceInfo, *terraform.InstanceState) (*terraform.InstanceState, error) { - atomic.AddInt32(&refreshCount, 1) - return &terraform.InstanceState{ID: "foo"}, nil - } - - checkDestroy := false - checkStep := false - - checkDestroyFn := func(*terraform.State) error { - checkDestroy = true - return nil - } - - checkStepFn := func(s *terraform.State) error { - checkStep = true - - rs, ok := s.RootModule().Resources["test_instance.foo"] - if !ok { - t.Error("test_instance.foo is not present") - return nil - } - is := rs.Primary - if is.ID != "foo" { - t.Errorf("bad check ID: %s", is.ID) - } - - return nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - CheckDestroy: checkDestroyFn, - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - Check: checkStepFn, - }, - }, - }) - - if mt.failed() { - t.Fatalf("test failed: %s", mt.failMessage()) - } - if mt.ParallelCalled { - t.Fatal("Parallel() called") - } - if !checkStep { - t.Fatal("didn't call check for step") - } - if !checkDestroy { - t.Fatal("didn't call check for destroy") - } - if !mp.TestResetCalled { - t.Fatal("didn't call TestReset") - } -} - -func TestTest_plan_only(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.ApplyReturn = &terraform.InstanceState{ - ID: "foo", - } - - checkDestroy := false - - checkDestroyFn := func(*terraform.State) error { - checkDestroy = true - return nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - CheckDestroy: checkDestroyFn, - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - PlanOnly: true, - ExpectNonEmptyPlan: false, - }, - }, - }) - - if !mt.failed() { - t.Fatal("test should've failed") - } - - expected := `Step 0 error: After applying this step, the plan was not empty: - -DIFF: - -CREATE: test_instance.foo - foo: "" => "bar" - -STATE: - -` - - if mt.failMessage() != expected { - t.Fatalf("Expected message: %s\n\ngot:\n\n%s", expected, mt.failMessage()) - } - - if !checkDestroy { - t.Fatal("didn't call check for destroy") - } -} - -func TestTest_idRefresh(t *testing.T) { - t.Skip("test requires new provider implementation") - - // Refresh count should be 3: - // 1.) initial Ref/Plan/Apply - // 2.) post Ref/Plan/Apply for plan-check - // 3.) id refresh check - var expectedRefresh int32 = 3 - - mp := testProvider() - mp.DiffReturn = nil - - mp.ApplyFn = func( - info *terraform.InstanceInfo, - state *terraform.InstanceState, - diff *terraform.InstanceDiff) (*terraform.InstanceState, error) { - if !diff.Destroy { - return &terraform.InstanceState{ - ID: "foo", - }, nil - } - - return nil, nil - } - - var refreshCount int32 - mp.RefreshFn = func(*terraform.InstanceInfo, *terraform.InstanceState) (*terraform.InstanceState, error) { - atomic.AddInt32(&refreshCount, 1) - return &terraform.InstanceState{ID: "foo"}, nil - } - - mt := new(mockT) - Test(mt, TestCase{ - IDRefreshName: "test_instance.foo", - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - }, - }, - }) - - if mt.failed() { - t.Fatalf("test failed: %s", mt.failMessage()) - } - - // See declaration of expectedRefresh for why that number - if refreshCount != expectedRefresh { - t.Fatalf("bad refresh count: %d", refreshCount) - } -} - -func TestTest_idRefreshCustomName(t *testing.T) { - t.Skip("test requires new provider implementation") - - // Refresh count should be 3: - // 1.) initial Ref/Plan/Apply - // 2.) post Ref/Plan/Apply for plan-check - // 3.) id refresh check - var expectedRefresh int32 = 3 - - mp := testProvider() - mp.DiffReturn = nil - - mp.ApplyFn = func( - info *terraform.InstanceInfo, - state *terraform.InstanceState, - diff *terraform.InstanceDiff) (*terraform.InstanceState, error) { - if !diff.Destroy { - return &terraform.InstanceState{ - ID: "foo", - }, nil - } - - return nil, nil - } - - var refreshCount int32 - mp.RefreshFn = func(*terraform.InstanceInfo, *terraform.InstanceState) (*terraform.InstanceState, error) { - atomic.AddInt32(&refreshCount, 1) - return &terraform.InstanceState{ID: "foo"}, nil - } - - mt := new(mockT) - Test(mt, TestCase{ - IDRefreshName: "test_instance.foo", - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - }, - }, - }) - - if mt.failed() { - t.Fatalf("test failed: %s", mt.failMessage()) - } - - // See declaration of expectedRefresh for why that number - if refreshCount != expectedRefresh { - t.Fatalf("bad refresh count: %d", refreshCount) - } -} - -func TestTest_idRefreshFail(t *testing.T) { - t.Skip("test requires new provider implementation") - - // Refresh count should be 3: - // 1.) initial Ref/Plan/Apply - // 2.) post Ref/Plan/Apply for plan-check - // 3.) id refresh check - var expectedRefresh int32 = 3 - - mp := testProvider() - mp.DiffReturn = nil - - mp.ApplyFn = func( - info *terraform.InstanceInfo, - state *terraform.InstanceState, - diff *terraform.InstanceDiff) (*terraform.InstanceState, error) { - if !diff.Destroy { - return &terraform.InstanceState{ - ID: "foo", - }, nil - } - - return nil, nil - } - - var refreshCount int32 - mp.RefreshFn = func(*terraform.InstanceInfo, *terraform.InstanceState) (*terraform.InstanceState, error) { - atomic.AddInt32(&refreshCount, 1) - if atomic.LoadInt32(&refreshCount) == expectedRefresh-1 { - return &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{"foo": "bar"}, - }, nil - } else if atomic.LoadInt32(&refreshCount) < expectedRefresh { - return &terraform.InstanceState{ID: "foo"}, nil - } else { - return nil, nil - } - } - - mt := new(mockT) - Test(mt, TestCase{ - IDRefreshName: "test_instance.foo", - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - }, - }, - }) - - if !mt.failed() { - t.Fatal("test didn't fail") - } - t.Logf("failure reason: %s", mt.failMessage()) - - // See declaration of expectedRefresh for why that number - if refreshCount != expectedRefresh { - t.Fatalf("bad refresh count: %d", refreshCount) - } -} - -func TestTest_empty(t *testing.T) { - t.Skip("test requires new provider implementation") - - destroyCalled := false - checkDestroyFn := func(*terraform.State) error { - destroyCalled = true - return nil - } - - mt := new(mockT) - Test(mt, TestCase{ - CheckDestroy: checkDestroyFn, - }) - - if mt.failed() { - t.Fatal("test failed") - } - if destroyCalled { - t.Fatal("should not call check destroy if there is no steps") - } -} - -func TestTest_noEnv(t *testing.T) { - t.Skip("test requires new provider implementation") - - // Unset the variable - if err := os.Setenv(TestEnvVar, ""); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Setenv(TestEnvVar, "1") - - mt := new(mockT) - Test(mt, TestCase{}) - - if !mt.SkipCalled { - t.Fatal("skip not called") - } -} - -func TestTest_preCheck(t *testing.T) { - t.Skip("test requires new provider implementation") - - called := false - - mt := new(mockT) - Test(mt, TestCase{ - PreCheck: func() { called = true }, - }) - - if !called { - t.Fatal("precheck should be called") - } -} - -func TestTest_skipFunc(t *testing.T) { - t.Skip("test requires new provider implementation") - - preCheckCalled := false - skipped := false - - mp := testProvider() - mp.ApplyReturn = &terraform.InstanceState{ - ID: "foo", - } - - checkStepFn := func(*terraform.State) error { - return fmt.Errorf("error") - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - PreCheck: func() { preCheckCalled = true }, - Steps: []TestStep{ - { - Config: testConfigStr, - Check: checkStepFn, - SkipFunc: func() (bool, error) { skipped = true; return true, nil }, - }, - }, - }) - - if mt.failed() { - t.Fatal("Expected check to be skipped") - } - - if !preCheckCalled { - t.Fatal("precheck should be called") - } - if !skipped { - t.Fatal("SkipFunc should be called") - } -} - -func TestTest_stepError(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.ApplyReturn = &terraform.InstanceState{ - ID: "foo", - } - - checkDestroy := false - - checkDestroyFn := func(*terraform.State) error { - checkDestroy = true - return nil - } - - checkStepFn := func(*terraform.State) error { - return fmt.Errorf("error") - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - CheckDestroy: checkDestroyFn, - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - Check: checkStepFn, - }, - }, - }) - - if !mt.failed() { - t.Fatal("test should've failed") - } - expected := "Step 0 error: Check failed: error" - if mt.failMessage() != expected { - t.Fatalf("Expected message: %s\n\ngot:\n\n%s", expected, mt.failMessage()) - } - - if !checkDestroy { - t.Fatal("didn't call check for destroy") - } -} - -func TestTest_factoryError(t *testing.T) { - resourceFactoryError := fmt.Errorf("resource factory error") - - factory := func() (terraform.ResourceProvider, error) { - return nil, resourceFactoryError - } - - mt := new(mockT) - Test(mt, TestCase{ - ProviderFactories: map[string]terraform.ResourceProviderFactory{ - "test": factory, - }, - Steps: []TestStep{ - TestStep{ - ExpectError: regexp.MustCompile("resource factory error"), - }, - }, - }) - - if !mt.failed() { - t.Fatal("test should've failed") - } -} - -func TestTest_resetError(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := &resetProvider{ - MockResourceProvider: testProvider(), - TestResetError: fmt.Errorf("provider reset error"), - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - Steps: []TestStep{ - TestStep{ - ExpectError: regexp.MustCompile("provider reset error"), - }, - }, - }) - - if !mt.failed() { - t.Fatal("test should've failed") - } -} - -func TestTest_expectError(t *testing.T) { - t.Skip("test requires new provider implementation") - - cases := []struct { - name string - planErr bool - applyErr bool - badErr bool - }{ - { - name: "successful apply", - planErr: false, - applyErr: false, - }, - { - name: "bad plan", - planErr: true, - applyErr: false, - }, - { - name: "bad apply", - planErr: false, - applyErr: true, - }, - { - name: "bad plan, bad err", - planErr: true, - applyErr: false, - badErr: true, - }, - { - name: "bad apply, bad err", - planErr: false, - applyErr: true, - badErr: true, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - mp := testProvider() - expectedText := "test provider error" - var errText string - if tc.badErr { - errText = "wrong provider error" - } else { - errText = expectedText - } - noErrText := "no error received, but expected a match to" - if tc.planErr { - mp.DiffReturnError = errors.New(errText) - } - if tc.applyErr { - mp.ApplyReturnError = errors.New(errText) - } - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - ExpectError: regexp.MustCompile(expectedText), - Check: func(*terraform.State) error { return nil }, - ExpectNonEmptyPlan: true, - }, - }, - }, - ) - if mt.FatalCalled { - t.Fatalf("fatal: %+v", mt.FatalArgs) - } - switch { - case len(mt.ErrorArgs) < 1 && !tc.planErr && !tc.applyErr: - t.Fatalf("expected error, got none") - case !tc.planErr && !tc.applyErr: - for _, e := range mt.ErrorArgs { - if regexp.MustCompile(noErrText).MatchString(fmt.Sprintf("%v", e)) { - return - } - } - t.Fatalf("expected error to match %s, got %+v", noErrText, mt.ErrorArgs) - case tc.badErr: - for _, e := range mt.ErrorArgs { - if regexp.MustCompile(expectedText).MatchString(fmt.Sprintf("%v", e)) { - return - } - } - t.Fatalf("expected error to match %s, got %+v", expectedText, mt.ErrorArgs) - } - }) - } -} - -func TestComposeAggregateTestCheckFunc(t *testing.T) { - check1 := func(s *terraform.State) error { - return errors.New("Error 1") - } - - check2 := func(s *terraform.State) error { - return errors.New("Error 2") - } - - f := ComposeAggregateTestCheckFunc(check1, check2) - err := f(nil) - if err == nil { - t.Fatalf("Expected errors") - } - - multi := err.(*multierror.Error) - if !strings.Contains(multi.Errors[0].Error(), "Error 1") { - t.Fatalf("Expected Error 1, Got %s", multi.Errors[0]) - } - if !strings.Contains(multi.Errors[1].Error(), "Error 2") { - t.Fatalf("Expected Error 2, Got %s", multi.Errors[1]) - } -} - -func TestComposeTestCheckFunc(t *testing.T) { - cases := []struct { - F []TestCheckFunc - Result string - }{ - { - F: []TestCheckFunc{ - func(*terraform.State) error { return nil }, - }, - Result: "", - }, - - { - F: []TestCheckFunc{ - func(*terraform.State) error { - return fmt.Errorf("error") - }, - func(*terraform.State) error { return nil }, - }, - Result: "Check 1/2 error: error", - }, - - { - F: []TestCheckFunc{ - func(*terraform.State) error { return nil }, - func(*terraform.State) error { - return fmt.Errorf("error") - }, - }, - Result: "Check 2/2 error: error", - }, - - { - F: []TestCheckFunc{ - func(*terraform.State) error { return nil }, - func(*terraform.State) error { return nil }, - }, - Result: "", - }, - } - - for i, tc := range cases { - f := ComposeTestCheckFunc(tc.F...) - err := f(nil) - if err == nil { - err = fmt.Errorf("") - } - if tc.Result != err.Error() { - t.Fatalf("Case %d bad: %s", i, err) - } - } -} - -// mockT implements TestT for testing -type mockT struct { - ErrorCalled bool - ErrorArgs []interface{} - FatalCalled bool - FatalArgs []interface{} - ParallelCalled bool - SkipCalled bool - SkipArgs []interface{} - - f bool -} - -func (t *mockT) Error(args ...interface{}) { - t.ErrorCalled = true - t.ErrorArgs = args - t.f = true -} - -func (t *mockT) Fatal(args ...interface{}) { - t.FatalCalled = true - t.FatalArgs = args - t.f = true -} - -func (t *mockT) Parallel() { - t.ParallelCalled = true -} - -func (t *mockT) Skip(args ...interface{}) { - t.SkipCalled = true - t.SkipArgs = args - t.f = true -} - -func (t *mockT) Name() string { - return "MockedName" -} - -func (t *mockT) failed() bool { - return t.f -} - -func (t *mockT) failMessage() string { - if t.FatalCalled { - return t.FatalArgs[0].(string) - } else if t.ErrorCalled { - return t.ErrorArgs[0].(string) - } else if t.SkipCalled { - return t.SkipArgs[0].(string) - } - - return "unknown" -} - -func testProvider() *terraform.MockResourceProvider { - mp := new(terraform.MockResourceProvider) - mp.DiffReturn = &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "bar", - }, - }, - } - mp.ResourcesReturn = []terraform.ResourceType{ - terraform.ResourceType{Name: "test_instance"}, - } - - return mp -} - -func TestTest_Main(t *testing.T) { - flag.Parse() - if *flagSweep == "" { - // Tests for the TestMain method used for Sweepers will panic without the -sweep - // flag specified. Mock the value for now - *flagSweep = "us-east-1" - } - - cases := []struct { - Name string - Sweepers map[string]*Sweeper - ExpectedRunList []string - SweepRun string - }{ - { - Name: "normal", - Sweepers: map[string]*Sweeper{ - "aws_dummy": &Sweeper{ - Name: "aws_dummy", - F: mockSweeperFunc, - }, - }, - ExpectedRunList: []string{"aws_dummy"}, - }, - { - Name: "with dep", - Sweepers: map[string]*Sweeper{ - "aws_dummy": &Sweeper{ - Name: "aws_dummy", - F: mockSweeperFunc, - }, - "aws_top": &Sweeper{ - Name: "aws_top", - Dependencies: []string{"aws_sub"}, - F: mockSweeperFunc, - }, - "aws_sub": &Sweeper{ - Name: "aws_sub", - F: mockSweeperFunc, - }, - }, - ExpectedRunList: []string{"aws_dummy", "aws_sub", "aws_top"}, - }, - { - Name: "with filter", - Sweepers: map[string]*Sweeper{ - "aws_dummy": &Sweeper{ - Name: "aws_dummy", - F: mockSweeperFunc, - }, - "aws_top": &Sweeper{ - Name: "aws_top", - Dependencies: []string{"aws_sub"}, - F: mockSweeperFunc, - }, - "aws_sub": &Sweeper{ - Name: "aws_sub", - F: mockSweeperFunc, - }, - }, - ExpectedRunList: []string{"aws_dummy"}, - SweepRun: "aws_dummy", - }, - { - Name: "with two filters", - Sweepers: map[string]*Sweeper{ - "aws_dummy": &Sweeper{ - Name: "aws_dummy", - F: mockSweeperFunc, - }, - "aws_top": &Sweeper{ - Name: "aws_top", - Dependencies: []string{"aws_sub"}, - F: mockSweeperFunc, - }, - "aws_sub": &Sweeper{ - Name: "aws_sub", - F: mockSweeperFunc, - }, - }, - ExpectedRunList: []string{"aws_dummy", "aws_sub"}, - SweepRun: "aws_dummy,aws_sub", - }, - { - Name: "with dep and filter", - Sweepers: map[string]*Sweeper{ - "aws_dummy": &Sweeper{ - Name: "aws_dummy", - F: mockSweeperFunc, - }, - "aws_top": &Sweeper{ - Name: "aws_top", - Dependencies: []string{"aws_sub"}, - F: mockSweeperFunc, - }, - "aws_sub": &Sweeper{ - Name: "aws_sub", - F: mockSweeperFunc, - }, - }, - ExpectedRunList: []string{"aws_top", "aws_sub"}, - SweepRun: "aws_top", - }, - { - Name: "filter and none", - Sweepers: map[string]*Sweeper{ - "aws_dummy": &Sweeper{ - Name: "aws_dummy", - F: mockSweeperFunc, - }, - "aws_top": &Sweeper{ - Name: "aws_top", - Dependencies: []string{"aws_sub"}, - F: mockSweeperFunc, - }, - "aws_sub": &Sweeper{ - Name: "aws_sub", - F: mockSweeperFunc, - }, - }, - SweepRun: "none", - }, - } - - for _, tc := range cases { - // reset sweepers - sweeperFuncs = map[string]*Sweeper{} - - t.Run(tc.Name, func(t *testing.T) { - for n, s := range tc.Sweepers { - AddTestSweepers(n, s) - } - *flagSweepRun = tc.SweepRun - - TestMain(&testing.M{}) - - // get list of tests ran from sweeperRunList keys - var keys []string - for k, _ := range sweeperRunList { - keys = append(keys, k) - } - - sort.Strings(keys) - sort.Strings(tc.ExpectedRunList) - if !reflect.DeepEqual(keys, tc.ExpectedRunList) { - t.Fatalf("Expected keys mismatch, expected:\n%#v\ngot:\n%#v\n", tc.ExpectedRunList, keys) - } - }) - } -} - -func mockSweeperFunc(s string) error { - return nil -} - -func TestTest_Taint(t *testing.T) { - t.Skip("test requires new provider implementation") - - mp := testProvider() - mp.DiffFn = func( - _ *terraform.InstanceInfo, - state *terraform.InstanceState, - _ *terraform.ResourceConfig, - ) (*terraform.InstanceDiff, error) { - return &terraform.InstanceDiff{ - DestroyTainted: state.Tainted, - }, nil - } - - mp.ApplyFn = func( - info *terraform.InstanceInfo, - state *terraform.InstanceState, - diff *terraform.InstanceDiff, - ) (*terraform.InstanceState, error) { - var id string - switch { - case diff.Destroy && !diff.DestroyTainted: - return nil, nil - case diff.DestroyTainted: - id = "tainted" - default: - id = "not_tainted" - } - - return &terraform.InstanceState{ - ID: id, - }, nil - } - - mp.RefreshFn = func( - _ *terraform.InstanceInfo, - state *terraform.InstanceState, - ) (*terraform.InstanceState, error) { - return state, nil - } - - mt := new(mockT) - Test(mt, TestCase{ - Providers: map[string]terraform.ResourceProvider{ - "test": mp, - }, - Steps: []TestStep{ - TestStep{ - Config: testConfigStr, - Check: func(s *terraform.State) error { - rs := s.RootModule().Resources["test_instance.foo"] - if rs.Primary.ID != "not_tainted" { - return fmt.Errorf("expected not_tainted, got %s", rs.Primary.ID) - } - return nil - }, - }, - TestStep{ - Taint: []string{"test_instance.foo"}, - Config: testConfigStr, - Check: func(s *terraform.State) error { - rs := s.RootModule().Resources["test_instance.foo"] - if rs.Primary.ID != "tainted" { - return fmt.Errorf("expected tainted, got %s", rs.Primary.ID) - } - return nil - }, - }, - TestStep{ - Taint: []string{"test_instance.fooo"}, - Config: testConfigStr, - ExpectError: regexp.MustCompile("resource \"test_instance.fooo\" not found in state"), - }, - }, - }) - - if mt.failed() { - t.Fatalf("test failure: %s", mt.failMessage()) - } -} - -const testConfigStr = ` -resource "test_instance" "foo" {} -` - -const testConfigStrProvider = ` -provider "test" {} -` - -func TestCheckResourceAttr_empty(t *testing.T) { - s := terraform.NewState() - s.AddModuleState(&terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_resource": &terraform.ResourceState{ - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "empty_list.#": "0", - "empty_map.%": "0", - }, - }, - }, - }, - }) - - for _, key := range []string{ - "empty_list.#", - "empty_map.%", - "missing_list.#", - "missing_map.%", - } { - t.Run(key, func(t *testing.T) { - check := TestCheckResourceAttr("test_resource", key, "0") - if err := check(s); err != nil { - t.Fatal(err) - } - }) - } -} - -func TestCheckNoResourceAttr_empty(t *testing.T) { - s := terraform.NewState() - s.AddModuleState(&terraform.ModuleState{ - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test_resource": &terraform.ResourceState{ - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "empty_list.#": "0", - "empty_map.%": "0", - }, - }, - }, - }, - }) - - for _, key := range []string{ - "empty_list.#", - "empty_map.%", - "missing_list.#", - "missing_map.%", - } { - t.Run(key, func(t *testing.T) { - check := TestCheckNoResourceAttr("test_resource", key) - if err := check(s); err != nil { - t.Fatal(err) - } - }) - } -} - -func TestTestCheckResourceAttrPair(t *testing.T) { - tests := map[string]struct { - state *terraform.State - wantErr string - }{ - "exist match": { - &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test.a": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "a": "boop", - }, - }, - }, - "test.b": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "b": "boop", - }, - }, - }, - }, - }, - }, - }, - ``, - }, - "nonexist match": { - &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test.a": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - "test.b": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - }, - }, - }, - }, - ``, - }, - "exist nonmatch": { - &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test.a": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "a": "beep", - }, - }, - }, - "test.b": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "b": "boop", - }, - }, - }, - }, - }, - }, - }, - `test.a: Attribute 'a' expected "boop", got "beep"`, - }, - "inconsistent exist a": { - &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test.a": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "a": "beep", - }, - }, - }, - "test.b": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - }, - }, - }, - }, - `test.a: Attribute "a" is "beep", but "b" is not set in test.b`, - }, - "inconsistent exist b": { - &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test.a": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - "test.b": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "b": "boop", - }, - }, - }, - }, - }, - }, - }, - `test.a: Attribute "a" not set, but "b" is set in test.b as "boop"`, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - fn := TestCheckResourceAttrPair("test.a", "a", "test.b", "b") - err := fn(test.state) - - if test.wantErr != "" { - if err == nil { - t.Fatalf("succeeded; want error\nwant: %s", test.wantErr) - } - if got, want := err.Error(), test.wantErr; got != want { - t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) - } - return - } - - if err != nil { - t.Fatalf("failed; want success\ngot: %s", err.Error()) - } - }) - } -} - -func TestTestCheckResourceAttrPairCount(t *testing.T) { - tests := map[string]struct { - state *terraform.State - attr string - wantErr string - }{ - "unset and 0 equal list": { - &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test.a": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "a.#": "0", - }, - }, - }, - "test.b": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - }, - }, - }, - }, - "a.#", - ``, - }, - "unset and 0 equal map": { - &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test.a": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "a.%": "0", - }, - }, - }, - "test.b": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - }, - }, - }, - }, - "a.%", - ``, - }, - "count equal": { - &terraform.State{ - Modules: []*terraform.ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*terraform.ResourceState{ - "test.a": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "a.%": "1", - }, - }, - }, - "test.b": { - Primary: &terraform.InstanceState{ - Attributes: map[string]string{ - "a.%": "1", - }}, - }, - }, - }, - }, - }, - "a.%", - ``, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - fn := TestCheckResourceAttrPair("test.a", test.attr, "test.b", test.attr) - err := fn(test.state) - - if test.wantErr != "" { - if err == nil { - t.Fatalf("succeeded; want error\nwant: %s", test.wantErr) - } - if got, want := err.Error(), test.wantErr; got != want { - t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) - } - return - } - - if err != nil { - t.Fatalf("failed; want success\ngot: %s", err.Error()) - } - }) - } -} diff --git a/helper/resource/wait.go b/helper/resource/wait.go deleted file mode 100644 index e56a5155d..000000000 --- a/helper/resource/wait.go +++ /dev/null @@ -1,84 +0,0 @@ -package resource - -import ( - "sync" - "time" -) - -// Retry is a basic wrapper around StateChangeConf that will just retry -// a function until it no longer returns an error. -func Retry(timeout time.Duration, f RetryFunc) error { - // These are used to pull the error out of the function; need a mutex to - // avoid a data race. - var resultErr error - var resultErrMu sync.Mutex - - c := &StateChangeConf{ - Pending: []string{"retryableerror"}, - Target: []string{"success"}, - Timeout: timeout, - MinTimeout: 500 * time.Millisecond, - Refresh: func() (interface{}, string, error) { - rerr := f() - - resultErrMu.Lock() - defer resultErrMu.Unlock() - - if rerr == nil { - resultErr = nil - return 42, "success", nil - } - - resultErr = rerr.Err - - if rerr.Retryable { - return 42, "retryableerror", nil - } - return nil, "quit", rerr.Err - }, - } - - _, waitErr := c.WaitForState() - - // Need to acquire the lock here to be able to avoid race using resultErr as - // the return value - resultErrMu.Lock() - defer resultErrMu.Unlock() - - // resultErr may be nil because the wait timed out and resultErr was never - // set; this is still an error - if resultErr == nil { - return waitErr - } - // resultErr takes precedence over waitErr if both are set because it is - // more likely to be useful - return resultErr -} - -// RetryFunc is the function retried until it succeeds. -type RetryFunc func() *RetryError - -// RetryError is the required return type of RetryFunc. It forces client code -// to choose whether or not a given error is retryable. -type RetryError struct { - Err error - Retryable bool -} - -// RetryableError is a helper to create a RetryError that's retryable from a -// given error. -func RetryableError(err error) *RetryError { - if err == nil { - return nil - } - return &RetryError{Err: err, Retryable: true} -} - -// NonRetryableError is a helper to create a RetryError that's _not_ retryable -// from a given error. -func NonRetryableError(err error) *RetryError { - if err == nil { - return nil - } - return &RetryError{Err: err, Retryable: false} -} diff --git a/helper/resource/wait_test.go b/helper/resource/wait_test.go deleted file mode 100644 index 526b21ae3..000000000 --- a/helper/resource/wait_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package resource - -import ( - "fmt" - "testing" - "time" -) - -func TestRetry(t *testing.T) { - t.Parallel() - - tries := 0 - f := func() *RetryError { - tries++ - if tries == 3 { - return nil - } - - return RetryableError(fmt.Errorf("error")) - } - - err := Retry(10*time.Second, f) - if err != nil { - t.Fatalf("err: %s", err) - } -} - -// make sure a slow StateRefreshFunc is allowed to complete after timeout -func TestRetry_grace(t *testing.T) { - t.Parallel() - - f := func() *RetryError { - time.Sleep(1 * time.Second) - return nil - } - - err := Retry(10*time.Millisecond, f) - if err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestRetry_timeout(t *testing.T) { - t.Parallel() - - f := func() *RetryError { - return RetryableError(fmt.Errorf("always")) - } - - err := Retry(1*time.Second, f) - if err == nil { - t.Fatal("should error") - } -} - -func TestRetry_hang(t *testing.T) { - old := refreshGracePeriod - refreshGracePeriod = 50 * time.Millisecond - defer func() { - refreshGracePeriod = old - }() - - f := func() *RetryError { - time.Sleep(2 * time.Second) - return nil - } - - err := Retry(50*time.Millisecond, f) - if err == nil { - t.Fatal("should error") - } -} - -func TestRetry_error(t *testing.T) { - t.Parallel() - - expected := fmt.Errorf("nope") - f := func() *RetryError { - return NonRetryableError(expected) - } - - errCh := make(chan error) - go func() { - errCh <- Retry(1*time.Second, f) - }() - - select { - case err := <-errCh: - if err != expected { - t.Fatalf("bad: %#v", err) - } - case <-time.After(5 * time.Second): - t.Fatal("timeout") - } -} diff --git a/helper/schema/README.md b/helper/schema/README.md deleted file mode 100644 index 28c83628e..000000000 --- a/helper/schema/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Terraform Helper Lib: schema - -The `schema` package provides a high-level interface for writing resource -providers for Terraform. - -If you're writing a resource provider, we recommend you use this package. - -The interface exposed by this package is much friendlier than trying to -write to the Terraform API directly. The core Terraform API is low-level -and built for maximum flexibility and control, whereas this library is built -as a framework around that to more easily write common providers. diff --git a/helper/schema/backend.go b/helper/schema/backend.go deleted file mode 100644 index 42c2bed92..000000000 --- a/helper/schema/backend.go +++ /dev/null @@ -1,200 +0,0 @@ -package schema - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform/tfdiags" - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/terraform" - ctyconvert "github.com/zclconf/go-cty/cty/convert" -) - -// Backend represents a partial backend.Backend implementation and simplifies -// the creation of configuration loading and validation. -// -// Unlike other schema structs such as Provider, this struct is meant to be -// embedded within your actual implementation. It provides implementations -// only for Input and Configure and gives you a method for accessing the -// configuration in the form of a ResourceData that you're expected to call -// from the other implementation funcs. -type Backend struct { - // Schema is the schema for the configuration of this backend. If this - // Backend has no configuration this can be omitted. - Schema map[string]*Schema - - // ConfigureFunc is called to configure the backend. Use the - // FromContext* methods to extract information from the context. - // This can be nil, in which case nothing will be called but the - // config will still be stored. - ConfigureFunc func(context.Context) error - - config *ResourceData -} - -var ( - backendConfigKey = contextKey("backend config") -) - -// FromContextBackendConfig extracts a ResourceData with the configuration -// from the context. This should only be called by Backend functions. -func FromContextBackendConfig(ctx context.Context) *ResourceData { - return ctx.Value(backendConfigKey).(*ResourceData) -} - -func (b *Backend) ConfigSchema() *configschema.Block { - // This is an alias of CoreConfigSchema just to implement the - // backend.Backend interface. - return b.CoreConfigSchema() -} - -func (b *Backend) PrepareConfig(configVal cty.Value) (cty.Value, tfdiags.Diagnostics) { - if b == nil { - return configVal, nil - } - var diags tfdiags.Diagnostics - var err error - - // In order to use Transform below, this needs to be filled out completely - // according the schema. - configVal, err = b.CoreConfigSchema().CoerceValue(configVal) - if err != nil { - return configVal, diags.Append(err) - } - - // lookup any required, top-level attributes that are Null, and see if we - // have a Default value available. - configVal, err = cty.Transform(configVal, func(path cty.Path, val cty.Value) (cty.Value, error) { - // we're only looking for top-level attributes - if len(path) != 1 { - return val, nil - } - - // nothing to do if we already have a value - if !val.IsNull() { - return val, nil - } - - // get the Schema definition for this attribute - getAttr, ok := path[0].(cty.GetAttrStep) - // these should all exist, but just ignore anything strange - if !ok { - return val, nil - } - - attrSchema := b.Schema[getAttr.Name] - // continue to ignore anything that doesn't match - if attrSchema == nil { - return val, nil - } - - // this is deprecated, so don't set it - if attrSchema.Deprecated != "" || attrSchema.Removed != "" { - return val, nil - } - - // find a default value if it exists - def, err := attrSchema.DefaultValue() - if err != nil { - diags = diags.Append(fmt.Errorf("error getting default for %q: %s", getAttr.Name, err)) - return val, err - } - - // no default - if def == nil { - return val, nil - } - - // create a cty.Value and make sure it's the correct type - tmpVal := hcl2shim.HCL2ValueFromConfigValue(def) - - // helper/schema used to allow setting "" to a bool - if val.Type() == cty.Bool && tmpVal.RawEquals(cty.StringVal("")) { - // return a warning about the conversion - diags = diags.Append("provider set empty string as default value for bool " + getAttr.Name) - tmpVal = cty.False - } - - val, err = ctyconvert.Convert(tmpVal, val.Type()) - if err != nil { - diags = diags.Append(fmt.Errorf("error setting default for %q: %s", getAttr.Name, err)) - } - - return val, err - }) - if err != nil { - // any error here was already added to the diagnostics - return configVal, diags - } - - shimRC := b.shimConfig(configVal) - warns, errs := schemaMap(b.Schema).Validate(shimRC) - for _, warn := range warns { - diags = diags.Append(tfdiags.SimpleWarning(warn)) - } - for _, err := range errs { - diags = diags.Append(err) - } - return configVal, diags -} - -func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { - if b == nil { - return nil - } - - var diags tfdiags.Diagnostics - sm := schemaMap(b.Schema) - shimRC := b.shimConfig(obj) - - // Get a ResourceData for this configuration. To do this, we actually - // generate an intermediary "diff" although that is never exposed. - diff, err := sm.Diff(nil, shimRC, nil, nil, true) - if err != nil { - diags = diags.Append(err) - return diags - } - - data, err := sm.Data(nil, diff) - if err != nil { - diags = diags.Append(err) - return diags - } - b.config = data - - if b.ConfigureFunc != nil { - err = b.ConfigureFunc(context.WithValue( - context.Background(), backendConfigKey, data)) - if err != nil { - diags = diags.Append(err) - return diags - } - } - - return diags -} - -// shimConfig turns a new-style cty.Value configuration (which must be of -// an object type) into a minimal old-style *terraform.ResourceConfig object -// that should be populated enough to appease the not-yet-updated functionality -// in this package. This should be removed once everything is updated. -func (b *Backend) shimConfig(obj cty.Value) *terraform.ResourceConfig { - shimMap, ok := hcl2shim.ConfigValueFromHCL2(obj).(map[string]interface{}) - if !ok { - // If the configVal was nil, we still want a non-nil map here. - shimMap = map[string]interface{}{} - } - return &terraform.ResourceConfig{ - Config: shimMap, - Raw: shimMap, - } -} - -// Config returns the configuration. This is available after Configure is -// called. -func (b *Backend) Config() *ResourceData { - return b.config -} diff --git a/helper/schema/backend_test.go b/helper/schema/backend_test.go deleted file mode 100644 index 8b0336fe0..000000000 --- a/helper/schema/backend_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package schema - -import ( - "context" - "fmt" - "testing" - - "github.com/zclconf/go-cty/cty" -) - -func TestBackendPrepare(t *testing.T) { - cases := []struct { - Name string - B *Backend - Config map[string]cty.Value - Expect map[string]cty.Value - Err bool - }{ - { - "Basic required field", - &Backend{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Required: true, - Type: TypeString, - }, - }, - }, - map[string]cty.Value{}, - map[string]cty.Value{}, - true, - }, - - { - "Null config", - &Backend{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Required: true, - Type: TypeString, - }, - }, - }, - nil, - map[string]cty.Value{}, - true, - }, - - { - "Basic required field set", - &Backend{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Required: true, - Type: TypeString, - }, - }, - }, - map[string]cty.Value{ - "foo": cty.StringVal("bar"), - }, - map[string]cty.Value{ - "foo": cty.StringVal("bar"), - }, - false, - }, - - { - "unused default", - &Backend{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Optional: true, - Type: TypeString, - Default: "baz", - }, - }, - }, - map[string]cty.Value{ - "foo": cty.StringVal("bar"), - }, - map[string]cty.Value{ - "foo": cty.StringVal("bar"), - }, - false, - }, - - { - "default", - &Backend{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Default: "baz", - }, - }, - }, - map[string]cty.Value{}, - map[string]cty.Value{ - "foo": cty.StringVal("baz"), - }, - false, - }, - - { - "default func", - &Backend{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - DefaultFunc: func() (interface{}, error) { - return "baz", nil - }, - }, - }, - }, - map[string]cty.Value{}, - map[string]cty.Value{ - "foo": cty.StringVal("baz"), - }, - false, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - cfgVal := cty.NullVal(cty.Object(map[string]cty.Type{})) - if tc.Config != nil { - cfgVal = cty.ObjectVal(tc.Config) - } - configVal, diags := tc.B.PrepareConfig(cfgVal) - if diags.HasErrors() != tc.Err { - for _, d := range diags { - t.Error(d.Description()) - } - } - - if tc.Err { - return - } - - expect := cty.ObjectVal(tc.Expect) - if !expect.RawEquals(configVal) { - t.Fatalf("\nexpected: %#v\ngot: %#v\n", expect, configVal) - } - }) - } -} - -func TestBackendConfigure(t *testing.T) { - cases := []struct { - Name string - B *Backend - Config map[string]cty.Value - Err bool - }{ - { - "Basic config", - &Backend{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ConfigureFunc: func(ctx context.Context) error { - d := FromContextBackendConfig(ctx) - if d.Get("foo").(int) != 42 { - return fmt.Errorf("bad config data") - } - - return nil - }, - }, - map[string]cty.Value{ - "foo": cty.NumberIntVal(42), - }, - false, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - diags := tc.B.Configure(cty.ObjectVal(tc.Config)) - if diags.HasErrors() != tc.Err { - t.Errorf("wrong number of diagnostics") - } - }) - } -} diff --git a/helper/schema/core_schema.go b/helper/schema/core_schema.go deleted file mode 100644 index 6a53db1a7..000000000 --- a/helper/schema/core_schema.go +++ /dev/null @@ -1,309 +0,0 @@ -package schema - -import ( - "fmt" - - "github.com/hashicorp/terraform/configs/configschema" - "github.com/zclconf/go-cty/cty" -) - -// The functions and methods in this file are concerned with the conversion -// of this package's schema model into the slightly-lower-level schema model -// used by Terraform core for configuration parsing. - -// CoreConfigSchema lowers the receiver to the schema model expected by -// Terraform core. -// -// This lower-level model has fewer features than the schema in this package, -// describing only the basic structure of configuration and state values we -// expect. The full schemaMap from this package is still required for full -// validation, handling of default values, etc. -// -// This method presumes a schema that passes InternalValidate, and so may -// panic or produce an invalid result if given an invalid schemaMap. -func (m schemaMap) CoreConfigSchema() *configschema.Block { - if len(m) == 0 { - // We return an actual (empty) object here, rather than a nil, - // because a nil result would mean that we don't have a schema at - // all, rather than that we have an empty one. - return &configschema.Block{} - } - - ret := &configschema.Block{ - Attributes: map[string]*configschema.Attribute{}, - BlockTypes: map[string]*configschema.NestedBlock{}, - } - - for name, schema := range m { - if schema.Elem == nil { - ret.Attributes[name] = schema.coreConfigSchemaAttribute() - continue - } - if schema.Type == TypeMap { - // For TypeMap in particular, it isn't valid for Elem to be a - // *Resource (since that would be ambiguous in flatmap) and - // so Elem is treated as a TypeString schema if so. This matches - // how the field readers treat this situation, for compatibility - // with configurations targeting Terraform 0.11 and earlier. - if _, isResource := schema.Elem.(*Resource); isResource { - sch := *schema // shallow copy - sch.Elem = &Schema{ - Type: TypeString, - } - ret.Attributes[name] = sch.coreConfigSchemaAttribute() - continue - } - } - switch schema.ConfigMode { - case SchemaConfigModeAttr: - ret.Attributes[name] = schema.coreConfigSchemaAttribute() - case SchemaConfigModeBlock: - ret.BlockTypes[name] = schema.coreConfigSchemaBlock() - default: // SchemaConfigModeAuto, or any other invalid value - if schema.Computed && !schema.Optional { - // Computed-only schemas are always handled as attributes, - // because they never appear in configuration. - ret.Attributes[name] = schema.coreConfigSchemaAttribute() - continue - } - switch schema.Elem.(type) { - case *Schema, ValueType: - ret.Attributes[name] = schema.coreConfigSchemaAttribute() - case *Resource: - ret.BlockTypes[name] = schema.coreConfigSchemaBlock() - default: - // Should never happen for a valid schema - panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", schema.Elem)) - } - } - } - - return ret -} - -// coreConfigSchemaAttribute prepares a configschema.Attribute representation -// of a schema. This is appropriate only for primitives or collections whose -// Elem is an instance of Schema. Use coreConfigSchemaBlock for collections -// whose elem is a whole resource. -func (s *Schema) coreConfigSchemaAttribute() *configschema.Attribute { - // The Schema.DefaultFunc capability adds some extra weirdness here since - // it can be combined with "Required: true" to create a situation where - // required-ness is conditional. Terraform Core doesn't share this concept, - // so we must sniff for this possibility here and conditionally turn - // off the "Required" flag if it looks like the DefaultFunc is going - // to provide a value. - // This is not 100% true to the original interface of DefaultFunc but - // works well enough for the EnvDefaultFunc and MultiEnvDefaultFunc - // situations, which are the main cases we care about. - // - // Note that this also has a consequence for commands that return schema - // information for documentation purposes: running those for certain - // providers will produce different results depending on which environment - // variables are set. We accept that weirdness in order to keep this - // interface to core otherwise simple. - reqd := s.Required - opt := s.Optional - if reqd && s.DefaultFunc != nil { - v, err := s.DefaultFunc() - // We can't report errors from here, so we'll instead just force - // "Required" to false and let the provider try calling its - // DefaultFunc again during the validate step, where it can then - // return the error. - if err != nil || (err == nil && v != nil) { - reqd = false - opt = true - } - } - - return &configschema.Attribute{ - Type: s.coreConfigSchemaType(), - Optional: opt, - Required: reqd, - Computed: s.Computed, - Sensitive: s.Sensitive, - Description: s.Description, - } -} - -// coreConfigSchemaBlock prepares a configschema.NestedBlock representation of -// a schema. This is appropriate only for collections whose Elem is an instance -// of Resource, and will panic otherwise. -func (s *Schema) coreConfigSchemaBlock() *configschema.NestedBlock { - ret := &configschema.NestedBlock{} - if nested := s.Elem.(*Resource).coreConfigSchema(); nested != nil { - ret.Block = *nested - } - switch s.Type { - case TypeList: - ret.Nesting = configschema.NestingList - case TypeSet: - ret.Nesting = configschema.NestingSet - case TypeMap: - ret.Nesting = configschema.NestingMap - default: - // Should never happen for a valid schema - panic(fmt.Errorf("invalid s.Type %s for s.Elem being resource", s.Type)) - } - - ret.MinItems = s.MinItems - ret.MaxItems = s.MaxItems - - if s.Required && s.MinItems == 0 { - // configschema doesn't have a "required" representation for nested - // blocks, but we can fake it by requiring at least one item. - ret.MinItems = 1 - } - if s.Optional && s.MinItems > 0 { - // Historically helper/schema would ignore MinItems if Optional were - // set, so we must mimic this behavior here to ensure that providers - // relying on that undocumented behavior can continue to operate as - // they did before. - ret.MinItems = 0 - } - if s.Computed && !s.Optional { - // MinItems/MaxItems are meaningless for computed nested blocks, since - // they are never set by the user anyway. This ensures that we'll never - // generate weird errors about them. - ret.MinItems = 0 - ret.MaxItems = 0 - } - - return ret -} - -// coreConfigSchemaType determines the core config schema type that corresponds -// to a particular schema's type. -func (s *Schema) coreConfigSchemaType() cty.Type { - switch s.Type { - case TypeString: - return cty.String - case TypeBool: - return cty.Bool - case TypeInt, TypeFloat: - // configschema doesn't distinguish int and float, so helper/schema - // will deal with this as an additional validation step after - // configuration has been parsed and decoded. - return cty.Number - case TypeList, TypeSet, TypeMap: - var elemType cty.Type - switch set := s.Elem.(type) { - case *Schema: - elemType = set.coreConfigSchemaType() - case ValueType: - // This represents a mistake in the provider code, but it's a - // common one so we'll just shim it. - elemType = (&Schema{Type: set}).coreConfigSchemaType() - case *Resource: - // By default we construct a NestedBlock in this case, but this - // behavior is selected either for computed-only schemas or - // when ConfigMode is explicitly SchemaConfigModeBlock. - // See schemaMap.CoreConfigSchema for the exact rules. - elemType = set.coreConfigSchema().ImpliedType() - default: - if set != nil { - // Should never happen for a valid schema - panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", s.Elem)) - } - // Some pre-existing schemas assume string as default, so we need - // to be compatible with them. - elemType = cty.String - } - switch s.Type { - case TypeList: - return cty.List(elemType) - case TypeSet: - return cty.Set(elemType) - case TypeMap: - return cty.Map(elemType) - default: - // can never get here in practice, due to the case we're inside - panic("invalid collection type") - } - default: - // should never happen for a valid schema - panic(fmt.Errorf("invalid Schema.Type %s", s.Type)) - } -} - -// CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema on -// the resource's schema. CoreConfigSchema adds the implicitly required "id" -// attribute for top level resources if it doesn't exist. -func (r *Resource) CoreConfigSchema() *configschema.Block { - block := r.coreConfigSchema() - - if block.Attributes == nil { - block.Attributes = map[string]*configschema.Attribute{} - } - - // Add the implicitly required "id" field if it doesn't exist - if block.Attributes["id"] == nil { - block.Attributes["id"] = &configschema.Attribute{ - Type: cty.String, - Optional: true, - Computed: true, - } - } - - _, timeoutsAttr := block.Attributes[TimeoutsConfigKey] - _, timeoutsBlock := block.BlockTypes[TimeoutsConfigKey] - - // Insert configured timeout values into the schema, as long as the schema - // didn't define anything else by that name. - if r.Timeouts != nil && !timeoutsAttr && !timeoutsBlock { - timeouts := configschema.Block{ - Attributes: map[string]*configschema.Attribute{}, - } - - if r.Timeouts.Create != nil { - timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{ - Type: cty.String, - Optional: true, - } - } - - if r.Timeouts.Read != nil { - timeouts.Attributes[TimeoutRead] = &configschema.Attribute{ - Type: cty.String, - Optional: true, - } - } - - if r.Timeouts.Update != nil { - timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{ - Type: cty.String, - Optional: true, - } - } - - if r.Timeouts.Delete != nil { - timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{ - Type: cty.String, - Optional: true, - } - } - - if r.Timeouts.Default != nil { - timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{ - Type: cty.String, - Optional: true, - } - } - - block.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{ - Nesting: configschema.NestingSingle, - Block: timeouts, - } - } - - return block -} - -func (r *Resource) coreConfigSchema() *configschema.Block { - return schemaMap(r.Schema).CoreConfigSchema() -} - -// CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema -// on the backends's schema. -func (r *Backend) CoreConfigSchema() *configschema.Block { - return schemaMap(r.Schema).CoreConfigSchema() -} diff --git a/helper/schema/core_schema_test.go b/helper/schema/core_schema_test.go deleted file mode 100644 index 7d4b32e01..000000000 --- a/helper/schema/core_schema_test.go +++ /dev/null @@ -1,458 +0,0 @@ -package schema - -import ( - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/configs/configschema" -) - -// add the implicit "id" attribute for test resources -func testResource(block *configschema.Block) *configschema.Block { - if block.Attributes == nil { - block.Attributes = make(map[string]*configschema.Attribute) - } - - if block.BlockTypes == nil { - block.BlockTypes = make(map[string]*configschema.NestedBlock) - } - - if block.Attributes["id"] == nil { - block.Attributes["id"] = &configschema.Attribute{ - Type: cty.String, - Optional: true, - Computed: true, - } - } - return block -} - -func TestSchemaMapCoreConfigSchema(t *testing.T) { - tests := map[string]struct { - Schema map[string]*Schema - Want *configschema.Block - }{ - "empty": { - map[string]*Schema{}, - testResource(&configschema.Block{}), - }, - "primitives": { - map[string]*Schema{ - "int": { - Type: TypeInt, - Required: true, - Description: "foo bar baz", - }, - "float": { - Type: TypeFloat, - Optional: true, - }, - "bool": { - Type: TypeBool, - Computed: true, - }, - "string": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "int": { - Type: cty.Number, - Required: true, - Description: "foo bar baz", - }, - "float": { - Type: cty.Number, - Optional: true, - }, - "bool": { - Type: cty.Bool, - Computed: true, - }, - "string": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - "simple collections": { - map[string]*Schema{ - "list": { - Type: TypeList, - Required: true, - Elem: &Schema{ - Type: TypeInt, - }, - }, - "set": { - Type: TypeSet, - Optional: true, - Elem: &Schema{ - Type: TypeString, - }, - }, - "map": { - Type: TypeMap, - Optional: true, - Elem: &Schema{ - Type: TypeBool, - }, - }, - "map_default_type": { - Type: TypeMap, - Optional: true, - // Maps historically don't have elements because we - // assumed they would be strings, so this needs to work - // for pre-existing schemas. - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "list": { - Type: cty.List(cty.Number), - Required: true, - }, - "set": { - Type: cty.Set(cty.String), - Optional: true, - }, - "map": { - Type: cty.Map(cty.Bool), - Optional: true, - }, - "map_default_type": { - Type: cty.Map(cty.String), - Optional: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - "incorrectly-specified collections": { - // Historically we tolerated setting a type directly as the Elem - // attribute, rather than a Schema object. This is common enough - // in existing provider code that we must support it as an alias - // for a schema object with the given type. - map[string]*Schema{ - "list": { - Type: TypeList, - Required: true, - Elem: TypeInt, - }, - "set": { - Type: TypeSet, - Optional: true, - Elem: TypeString, - }, - "map": { - Type: TypeMap, - Optional: true, - Elem: TypeBool, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "list": { - Type: cty.List(cty.Number), - Required: true, - }, - "set": { - Type: cty.Set(cty.String), - Optional: true, - }, - "map": { - Type: cty.Map(cty.Bool), - Optional: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - "sub-resource collections": { - map[string]*Schema{ - "list": { - Type: TypeList, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{}, - }, - MinItems: 1, - MaxItems: 2, - }, - "set": { - Type: TypeSet, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{}, - }, - }, - "map": { - Type: TypeMap, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{}, - }, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - // This one becomes a string attribute because helper/schema - // doesn't actually support maps of resource. The given - // "Elem" is just ignored entirely here, which is important - // because that is also true of the helper/schema logic and - // existing providers rely on this being ignored for - // correct operation. - "map": { - Type: cty.Map(cty.String), - Optional: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "list": { - Nesting: configschema.NestingList, - Block: configschema.Block{}, - MinItems: 1, - MaxItems: 2, - }, - "set": { - Nesting: configschema.NestingSet, - Block: configschema.Block{}, - MinItems: 1, // because schema is Required - }, - }, - }), - }, - "sub-resource collections minitems+optional": { - // This particular case is an odd one where the provider gives - // conflicting information about whether a sub-resource is required, - // by marking it as optional but also requiring one item. - // Historically the optional-ness "won" here, and so we must - // honor that for compatibility with providers that relied on this - // undocumented interaction. - map[string]*Schema{ - "list": { - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{}, - }, - MinItems: 1, - MaxItems: 1, - }, - "set": { - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{}, - }, - MinItems: 1, - MaxItems: 1, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{}, - BlockTypes: map[string]*configschema.NestedBlock{ - "list": { - Nesting: configschema.NestingList, - Block: configschema.Block{}, - MinItems: 0, - MaxItems: 1, - }, - "set": { - Nesting: configschema.NestingSet, - Block: configschema.Block{}, - MinItems: 0, - MaxItems: 1, - }, - }, - }), - }, - "sub-resource collections minitems+computed": { - map[string]*Schema{ - "list": { - Type: TypeList, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{}, - }, - MinItems: 1, - MaxItems: 1, - }, - "set": { - Type: TypeSet, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{}, - }, - MinItems: 1, - MaxItems: 1, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "list": { - Type: cty.List(cty.EmptyObject), - Computed: true, - }, - "set": { - Type: cty.Set(cty.EmptyObject), - Computed: true, - }, - }, - }), - }, - "nested attributes and blocks": { - map[string]*Schema{ - "foo": { - Type: TypeList, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeList, - Required: true, - Elem: &Schema{ - Type: TypeList, - Elem: &Schema{ - Type: TypeString, - }, - }, - }, - "baz": { - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{}, - }, - }, - }, - }, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{}, - BlockTypes: map[string]*configschema.NestedBlock{ - "foo": &configschema.NestedBlock{ - Nesting: configschema.NestingList, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": { - Type: cty.List(cty.List(cty.String)), - Required: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "baz": { - Nesting: configschema.NestingSet, - Block: configschema.Block{}, - }, - }, - }, - MinItems: 1, // because schema is Required - }, - }, - }), - }, - "sensitive": { - map[string]*Schema{ - "string": { - Type: TypeString, - Optional: true, - Sensitive: true, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "string": { - Type: cty.String, - Optional: true, - Sensitive: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - "conditionally required on": { - map[string]*Schema{ - "string": { - Type: TypeString, - Required: true, - DefaultFunc: func() (interface{}, error) { - return nil, nil - }, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "string": { - Type: cty.String, - Required: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - "conditionally required off": { - map[string]*Schema{ - "string": { - Type: TypeString, - Required: true, - DefaultFunc: func() (interface{}, error) { - // If we return a non-nil default then this overrides - // the "Required: true" for the purpose of building - // the core schema, so that core will ignore it not - // being set and let the provider handle it. - return "boop", nil - }, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "string": { - Type: cty.String, - Optional: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - "conditionally required error": { - map[string]*Schema{ - "string": { - Type: TypeString, - Required: true, - DefaultFunc: func() (interface{}, error) { - return nil, fmt.Errorf("placeholder error") - }, - }, - }, - testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "string": { - Type: cty.String, - Optional: true, // Just so we can progress to provider-driven validation and return the error there - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - got := (&Resource{Schema: test.Schema}).CoreConfigSchema() - if !cmp.Equal(got, test.Want, equateEmpty, typeComparer) { - t.Error(cmp.Diff(got, test.Want, equateEmpty, typeComparer)) - } - }) - } -} diff --git a/helper/schema/data_source_resource_shim.go b/helper/schema/data_source_resource_shim.go deleted file mode 100644 index 8d93750ae..000000000 --- a/helper/schema/data_source_resource_shim.go +++ /dev/null @@ -1,59 +0,0 @@ -package schema - -import ( - "fmt" -) - -// DataSourceResourceShim takes a Resource instance describing a data source -// (with a Read implementation and a Schema, at least) and returns a new -// Resource instance with additional Create and Delete implementations that -// allow the data source to be used as a resource. -// -// This is a backward-compatibility layer for data sources that were formerly -// read-only resources before the data source concept was added. It should not -// be used for any *new* data sources. -// -// The Read function for the data source *must* call d.SetId with a non-empty -// id in order for this shim to function as expected. -// -// The provided Resource instance, and its schema, will be modified in-place -// to make it suitable for use as a full resource. -func DataSourceResourceShim(name string, dataSource *Resource) *Resource { - // Recursively, in-place adjust the schema so that it has ForceNew - // on any user-settable resource. - dataSourceResourceShimAdjustSchema(dataSource.Schema) - - dataSource.Create = CreateFunc(dataSource.Read) - dataSource.Delete = func(d *ResourceData, meta interface{}) error { - d.SetId("") - return nil - } - dataSource.Update = nil // should already be nil, but let's make sure - - // FIXME: Link to some further docs either on the website or in the - // changelog, once such a thing exists. - dataSource.DeprecationMessage = fmt.Sprintf( - "using %s as a resource is deprecated; consider using the data source instead", - name, - ) - - return dataSource -} - -func dataSourceResourceShimAdjustSchema(schema map[string]*Schema) { - for _, s := range schema { - // If the attribute is configurable then it must be ForceNew, - // since we have no Update implementation. - if s.Required || s.Optional { - s.ForceNew = true - } - - // If the attribute is a nested resource, we need to recursively - // apply these same adjustments to it. - if s.Elem != nil { - if r, ok := s.Elem.(*Resource); ok { - dataSourceResourceShimAdjustSchema(r.Schema) - } - } - } -} diff --git a/helper/schema/equal.go b/helper/schema/equal.go deleted file mode 100644 index d5e20e038..000000000 --- a/helper/schema/equal.go +++ /dev/null @@ -1,6 +0,0 @@ -package schema - -// Equal is an interface that checks for deep equality between two objects. -type Equal interface { - Equal(interface{}) bool -} diff --git a/helper/schema/field_reader.go b/helper/schema/field_reader.go deleted file mode 100644 index 2a66a068f..000000000 --- a/helper/schema/field_reader.go +++ /dev/null @@ -1,343 +0,0 @@ -package schema - -import ( - "fmt" - "strconv" - "strings" -) - -// FieldReaders are responsible for decoding fields out of data into -// the proper typed representation. ResourceData uses this to query data -// out of multiple sources: config, state, diffs, etc. -type FieldReader interface { - ReadField([]string) (FieldReadResult, error) -} - -// FieldReadResult encapsulates all the resulting data from reading -// a field. -type FieldReadResult struct { - // Value is the actual read value. NegValue is the _negative_ value - // or the items that should be removed (if they existed). NegValue - // doesn't make sense for primitives but is important for any - // container types such as maps, sets, lists. - Value interface{} - ValueProcessed interface{} - - // Exists is true if the field was found in the data. False means - // it wasn't found if there was no error. - Exists bool - - // Computed is true if the field was found but the value - // is computed. - Computed bool -} - -// ValueOrZero returns the value of this result or the zero value of the -// schema type, ensuring a consistent non-nil return value. -func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} { - if r.Value != nil { - return r.Value - } - - return s.ZeroValue() -} - -// SchemasForFlatmapPath tries its best to find a sequence of schemas that -// the given dot-delimited attribute path traverses through. -func SchemasForFlatmapPath(path string, schemaMap map[string]*Schema) []*Schema { - parts := strings.Split(path, ".") - return addrToSchema(parts, schemaMap) -} - -// addrToSchema finds the final element schema for the given address -// and the given schema. It returns all the schemas that led to the final -// schema. These are in order of the address (out to in). -func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema { - current := &Schema{ - Type: typeObject, - Elem: schemaMap, - } - - // If we aren't given an address, then the user is requesting the - // full object, so we return the special value which is the full object. - if len(addr) == 0 { - return []*Schema{current} - } - - result := make([]*Schema, 0, len(addr)) - for len(addr) > 0 { - k := addr[0] - addr = addr[1:] - - REPEAT: - // We want to trim off the first "typeObject" since its not a - // real lookup that people do. i.e. []string{"foo"} in a structure - // isn't {typeObject, typeString}, its just a {typeString}. - if len(result) > 0 || current.Type != typeObject { - result = append(result, current) - } - - switch t := current.Type; t { - case TypeBool, TypeInt, TypeFloat, TypeString: - if len(addr) > 0 { - return nil - } - case TypeList, TypeSet: - isIndex := len(addr) > 0 && addr[0] == "#" - - switch v := current.Elem.(type) { - case *Resource: - current = &Schema{ - Type: typeObject, - Elem: v.Schema, - } - case *Schema: - current = v - case ValueType: - current = &Schema{Type: v} - default: - // we may not know the Elem type and are just looking for the - // index - if isIndex { - break - } - - if len(addr) == 0 { - // we've processed the address, so return what we've - // collected - return result - } - - if len(addr) == 1 { - if _, err := strconv.Atoi(addr[0]); err == nil { - // we're indexing a value without a schema. This can - // happen if the list is nested in another schema type. - // Default to a TypeString like we do with a map - current = &Schema{Type: TypeString} - break - } - } - - return nil - } - - // If we only have one more thing and the next thing - // is a #, then we're accessing the index which is always - // an int. - if isIndex { - current = &Schema{Type: TypeInt} - break - } - - case TypeMap: - if len(addr) > 0 { - switch v := current.Elem.(type) { - case ValueType: - current = &Schema{Type: v} - case *Schema: - current, _ = current.Elem.(*Schema) - default: - // maps default to string values. This is all we can have - // if this is nested in another list or map. - current = &Schema{Type: TypeString} - } - } - case typeObject: - // If we're already in the object, then we want to handle Sets - // and Lists specially. Basically, their next key is the lookup - // key (the set value or the list element). For these scenarios, - // we just want to skip it and move to the next element if there - // is one. - if len(result) > 0 { - lastType := result[len(result)-2].Type - if lastType == TypeSet || lastType == TypeList { - if len(addr) == 0 { - break - } - - k = addr[0] - addr = addr[1:] - } - } - - m := current.Elem.(map[string]*Schema) - val, ok := m[k] - if !ok { - return nil - } - - current = val - goto REPEAT - } - } - - return result -} - -// readListField is a generic method for reading a list field out of a -// a FieldReader. It does this based on the assumption that there is a key -// "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc. -// after that point. -func readListField( - r FieldReader, addr []string, schema *Schema) (FieldReadResult, error) { - addrPadded := make([]string, len(addr)+1) - copy(addrPadded, addr) - addrPadded[len(addrPadded)-1] = "#" - - // Get the number of elements in the list - countResult, err := r.ReadField(addrPadded) - if err != nil { - return FieldReadResult{}, err - } - if !countResult.Exists { - // No count, means we have no list - countResult.Value = 0 - } - - // If we have an empty list, then return an empty list - if countResult.Computed || countResult.Value.(int) == 0 { - return FieldReadResult{ - Value: []interface{}{}, - Exists: countResult.Exists, - Computed: countResult.Computed, - }, nil - } - - // Go through each count, and get the item value out of it - result := make([]interface{}, countResult.Value.(int)) - for i, _ := range result { - is := strconv.FormatInt(int64(i), 10) - addrPadded[len(addrPadded)-1] = is - rawResult, err := r.ReadField(addrPadded) - if err != nil { - return FieldReadResult{}, err - } - if !rawResult.Exists { - // This should never happen, because by the time the data - // gets to the FieldReaders, all the defaults should be set by - // Schema. - rawResult.Value = nil - } - - result[i] = rawResult.Value - } - - return FieldReadResult{ - Value: result, - Exists: true, - }, nil -} - -// readObjectField is a generic method for reading objects out of FieldReaders -// based on the assumption that building an address of []string{k, FIELD} -// will result in the proper field data. -func readObjectField( - r FieldReader, - addr []string, - schema map[string]*Schema) (FieldReadResult, error) { - result := make(map[string]interface{}) - exists := false - for field, s := range schema { - addrRead := make([]string, len(addr), len(addr)+1) - copy(addrRead, addr) - addrRead = append(addrRead, field) - rawResult, err := r.ReadField(addrRead) - if err != nil { - return FieldReadResult{}, err - } - if rawResult.Exists { - exists = true - } - - result[field] = rawResult.ValueOrZero(s) - } - - return FieldReadResult{ - Value: result, - Exists: exists, - }, nil -} - -// convert map values to the proper primitive type based on schema.Elem -func mapValuesToPrimitive(k string, m map[string]interface{}, schema *Schema) error { - elemType, err := getValueType(k, schema) - if err != nil { - return err - } - - switch elemType { - case TypeInt, TypeFloat, TypeBool: - for k, v := range m { - vs, ok := v.(string) - if !ok { - continue - } - - v, err := stringToPrimitive(vs, false, &Schema{Type: elemType}) - if err != nil { - return err - } - - m[k] = v - } - } - return nil -} - -func stringToPrimitive( - value string, computed bool, schema *Schema) (interface{}, error) { - var returnVal interface{} - switch schema.Type { - case TypeBool: - if value == "" { - returnVal = false - break - } - if computed { - break - } - - v, err := strconv.ParseBool(value) - if err != nil { - return nil, err - } - - returnVal = v - case TypeFloat: - if value == "" { - returnVal = 0.0 - break - } - if computed { - break - } - - v, err := strconv.ParseFloat(value, 64) - if err != nil { - return nil, err - } - - returnVal = v - case TypeInt: - if value == "" { - returnVal = 0 - break - } - if computed { - break - } - - v, err := strconv.ParseInt(value, 0, 0) - if err != nil { - return nil, err - } - - returnVal = int(v) - case TypeString: - returnVal = value - default: - panic(fmt.Sprintf("Unknown type: %s", schema.Type)) - } - - return returnVal, nil -} diff --git a/helper/schema/field_reader_config.go b/helper/schema/field_reader_config.go deleted file mode 100644 index 6ad3f13cb..000000000 --- a/helper/schema/field_reader_config.go +++ /dev/null @@ -1,353 +0,0 @@ -package schema - -import ( - "fmt" - "log" - "strconv" - "strings" - "sync" - - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/mapstructure" -) - -// ConfigFieldReader reads fields out of an untyped map[string]string to the -// best of its ability. It also applies defaults from the Schema. (The other -// field readers do not need default handling because they source fully -// populated data structures.) -type ConfigFieldReader struct { - Config *terraform.ResourceConfig - Schema map[string]*Schema - - indexMaps map[string]map[string]int - once sync.Once -} - -func (r *ConfigFieldReader) ReadField(address []string) (FieldReadResult, error) { - r.once.Do(func() { r.indexMaps = make(map[string]map[string]int) }) - return r.readField(address, false) -} - -func (r *ConfigFieldReader) readField( - address []string, nested bool) (FieldReadResult, error) { - schemaList := addrToSchema(address, r.Schema) - if len(schemaList) == 0 { - return FieldReadResult{}, nil - } - - if !nested { - // If we have a set anywhere in the address, then we need to - // read that set out in order and actually replace that part of - // the address with the real list index. i.e. set.50 might actually - // map to set.12 in the config, since it is in list order in the - // config, not indexed by set value. - for i, v := range schemaList { - // Sets are the only thing that cause this issue. - if v.Type != TypeSet { - continue - } - - // If we're at the end of the list, then we don't have to worry - // about this because we're just requesting the whole set. - if i == len(schemaList)-1 { - continue - } - - // If we're looking for the count, then ignore... - if address[i+1] == "#" { - continue - } - - indexMap, ok := r.indexMaps[strings.Join(address[:i+1], ".")] - if !ok { - // Get the set so we can get the index map that tells us the - // mapping of the hash code to the list index - _, err := r.readSet(address[:i+1], v) - if err != nil { - return FieldReadResult{}, err - } - indexMap = r.indexMaps[strings.Join(address[:i+1], ".")] - } - - index, ok := indexMap[address[i+1]] - if !ok { - return FieldReadResult{}, nil - } - - address[i+1] = strconv.FormatInt(int64(index), 10) - } - } - - k := strings.Join(address, ".") - schema := schemaList[len(schemaList)-1] - - // If we're getting the single element of a promoted list, then - // check to see if we have a single element we need to promote. - if address[len(address)-1] == "0" && len(schemaList) > 1 { - lastSchema := schemaList[len(schemaList)-2] - if lastSchema.Type == TypeList && lastSchema.PromoteSingle { - k := strings.Join(address[:len(address)-1], ".") - result, err := r.readPrimitive(k, schema) - if err == nil { - return result, nil - } - } - } - - if protoVersion5 { - switch schema.Type { - case TypeList, TypeSet, TypeMap, typeObject: - // Check if the value itself is unknown. - // The new protocol shims will add unknown values to this list of - // ComputedKeys. This is the only way we have to indicate that a - // collection is unknown in the config - for _, unknown := range r.Config.ComputedKeys { - if k == unknown { - log.Printf("[DEBUG] setting computed for %q from ComputedKeys", k) - return FieldReadResult{Computed: true, Exists: true}, nil - } - } - } - } - - switch schema.Type { - case TypeBool, TypeFloat, TypeInt, TypeString: - return r.readPrimitive(k, schema) - case TypeList: - // If we support promotion then we first check if we have a lone - // value that we must promote. - // a value that is alone. - if schema.PromoteSingle { - result, err := r.readPrimitive(k, schema.Elem.(*Schema)) - if err == nil && result.Exists { - result.Value = []interface{}{result.Value} - return result, nil - } - } - - return readListField(&nestedConfigFieldReader{r}, address, schema) - case TypeMap: - return r.readMap(k, schema) - case TypeSet: - return r.readSet(address, schema) - case typeObject: - return readObjectField( - &nestedConfigFieldReader{r}, - address, schema.Elem.(map[string]*Schema)) - default: - panic(fmt.Sprintf("Unknown type: %s", schema.Type)) - } -} - -func (r *ConfigFieldReader) readMap(k string, schema *Schema) (FieldReadResult, error) { - // We want both the raw value and the interpolated. We use the interpolated - // to store actual values and we use the raw one to check for - // computed keys. Actual values are obtained in the switch, depending on - // the type of the raw value. - mraw, ok := r.Config.GetRaw(k) - if !ok { - // check if this is from an interpolated field by seeing if it exists - // in the config - _, ok := r.Config.Get(k) - if !ok { - // this really doesn't exist - return FieldReadResult{}, nil - } - - // We couldn't fetch the value from a nested data structure, so treat the - // raw value as an interpolation string. The mraw value is only used - // for the type switch below. - mraw = "${INTERPOLATED}" - } - - result := make(map[string]interface{}) - computed := false - switch m := mraw.(type) { - case string: - // This is a map which has come out of an interpolated variable, so we - // can just get the value directly from config. Values cannot be computed - // currently. - v, _ := r.Config.Get(k) - - // If this isn't a map[string]interface, it must be computed. - mapV, ok := v.(map[string]interface{}) - if !ok { - return FieldReadResult{ - Exists: true, - Computed: true, - }, nil - } - - // Otherwise we can proceed as usual. - for i, iv := range mapV { - result[i] = iv - } - case []interface{}: - for i, innerRaw := range m { - for ik := range innerRaw.(map[string]interface{}) { - key := fmt.Sprintf("%s.%d.%s", k, i, ik) - if r.Config.IsComputed(key) { - computed = true - break - } - - v, _ := r.Config.Get(key) - result[ik] = v - } - } - case []map[string]interface{}: - for i, innerRaw := range m { - for ik := range innerRaw { - key := fmt.Sprintf("%s.%d.%s", k, i, ik) - if r.Config.IsComputed(key) { - computed = true - break - } - - v, _ := r.Config.Get(key) - result[ik] = v - } - } - case map[string]interface{}: - for ik := range m { - key := fmt.Sprintf("%s.%s", k, ik) - if r.Config.IsComputed(key) { - computed = true - break - } - - v, _ := r.Config.Get(key) - result[ik] = v - } - case nil: - // the map may have been empty on the configuration, so we leave the - // empty result - default: - panic(fmt.Sprintf("unknown type: %#v", mraw)) - } - - err := mapValuesToPrimitive(k, result, schema) - if err != nil { - return FieldReadResult{}, nil - } - - var value interface{} - if !computed { - value = result - } - - return FieldReadResult{ - Value: value, - Exists: true, - Computed: computed, - }, nil -} - -func (r *ConfigFieldReader) readPrimitive( - k string, schema *Schema) (FieldReadResult, error) { - raw, ok := r.Config.Get(k) - if !ok { - // Nothing in config, but we might still have a default from the schema - var err error - raw, err = schema.DefaultValue() - if err != nil { - return FieldReadResult{}, fmt.Errorf("%s, error loading default: %s", k, err) - } - - if raw == nil { - return FieldReadResult{}, nil - } - } - - var result string - if err := mapstructure.WeakDecode(raw, &result); err != nil { - return FieldReadResult{}, err - } - - computed := r.Config.IsComputed(k) - returnVal, err := stringToPrimitive(result, computed, schema) - if err != nil { - return FieldReadResult{}, err - } - - return FieldReadResult{ - Value: returnVal, - Exists: true, - Computed: computed, - }, nil -} - -func (r *ConfigFieldReader) readSet( - address []string, schema *Schema) (FieldReadResult, error) { - indexMap := make(map[string]int) - // Create the set that will be our result - set := schema.ZeroValue().(*Set) - - raw, err := readListField(&nestedConfigFieldReader{r}, address, schema) - if err != nil { - return FieldReadResult{}, err - } - if !raw.Exists { - return FieldReadResult{Value: set}, nil - } - - // If the list is computed, the set is necessarilly computed - if raw.Computed { - return FieldReadResult{ - Value: set, - Exists: true, - Computed: raw.Computed, - }, nil - } - - // Build up the set from the list elements - for i, v := range raw.Value.([]interface{}) { - // Check if any of the keys in this item are computed - computed := r.hasComputedSubKeys( - fmt.Sprintf("%s.%d", strings.Join(address, "."), i), schema) - - code := set.add(v, computed) - indexMap[code] = i - } - - r.indexMaps[strings.Join(address, ".")] = indexMap - - return FieldReadResult{ - Value: set, - Exists: true, - }, nil -} - -// hasComputedSubKeys walks through a schema and returns whether or not the -// given key contains any subkeys that are computed. -func (r *ConfigFieldReader) hasComputedSubKeys(key string, schema *Schema) bool { - prefix := key + "." - - switch t := schema.Elem.(type) { - case *Resource: - for k, schema := range t.Schema { - if r.Config.IsComputed(prefix + k) { - return true - } - - if r.hasComputedSubKeys(prefix+k, schema) { - return true - } - } - } - - return false -} - -// nestedConfigFieldReader is a funny little thing that just wraps a -// ConfigFieldReader to call readField when ReadField is called so that -// we don't recalculate the set rewrites in the address, which leads to -// an infinite loop. -type nestedConfigFieldReader struct { - Reader *ConfigFieldReader -} - -func (r *nestedConfigFieldReader) ReadField( - address []string) (FieldReadResult, error) { - return r.Reader.readField(address, true) -} diff --git a/helper/schema/field_reader_config_test.go b/helper/schema/field_reader_config_test.go deleted file mode 100644 index 5e2728250..000000000 --- a/helper/schema/field_reader_config_test.go +++ /dev/null @@ -1,540 +0,0 @@ -package schema - -import ( - "bytes" - "fmt" - "reflect" - "testing" - - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/helper/hashcode" - "github.com/hashicorp/terraform/terraform" -) - -func TestConfigFieldReader_impl(t *testing.T) { - var _ FieldReader = new(ConfigFieldReader) -} - -func TestConfigFieldReader(t *testing.T) { - testFieldReader(t, func(s map[string]*Schema) FieldReader { - return &ConfigFieldReader{ - Schema: s, - - Config: testConfig(t, map[string]interface{}{ - "bool": true, - "float": 3.1415, - "int": 42, - "string": "string", - - "list": []interface{}{"foo", "bar"}, - - "listInt": []interface{}{21, 42}, - - "map": map[string]interface{}{ - "foo": "bar", - "bar": "baz", - }, - "mapInt": map[string]interface{}{ - "one": "1", - "two": "2", - }, - "mapIntNestedSchema": map[string]interface{}{ - "one": "1", - "two": "2", - }, - "mapFloat": map[string]interface{}{ - "oneDotTwo": "1.2", - }, - "mapBool": map[string]interface{}{ - "True": "true", - "False": "false", - }, - - "set": []interface{}{10, 50}, - "setDeep": []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "foo", - }, - map[string]interface{}{ - "index": 50, - "value": "bar", - }, - }, - }), - } - }) -} - -// This contains custom table tests for our ConfigFieldReader -func TestConfigFieldReader_custom(t *testing.T) { - schema := map[string]*Schema{ - "bool": &Schema{ - Type: TypeBool, - }, - } - - cases := map[string]struct { - Addr []string - Result FieldReadResult - Config *terraform.ResourceConfig - Err bool - }{ - "basic": { - []string{"bool"}, - FieldReadResult{ - Value: true, - Exists: true, - }, - testConfig(t, map[string]interface{}{ - "bool": true, - }), - false, - }, - - "computed": { - []string{"bool"}, - FieldReadResult{ - Exists: true, - Computed: true, - }, - testConfig(t, map[string]interface{}{ - "bool": hcl2shim.UnknownVariableValue, - }), - false, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - r := &ConfigFieldReader{ - Schema: schema, - Config: tc.Config, - } - out, err := r.ReadField(tc.Addr) - if err != nil != tc.Err { - t.Fatalf("%s: err: %s", name, err) - } - if s, ok := out.Value.(*Set); ok { - // If it is a set, convert to a list so its more easily checked. - out.Value = s.List() - } - if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) - } - }) - } -} - -func TestConfigFieldReader_DefaultHandling(t *testing.T) { - schema := map[string]*Schema{ - "strWithDefault": &Schema{ - Type: TypeString, - Default: "ImADefault", - }, - "strWithDefaultFunc": &Schema{ - Type: TypeString, - DefaultFunc: func() (interface{}, error) { - return "FuncDefault", nil - }, - }, - } - - cases := map[string]struct { - Addr []string - Result FieldReadResult - Config *terraform.ResourceConfig - Err bool - }{ - "gets default value when no config set": { - []string{"strWithDefault"}, - FieldReadResult{ - Value: "ImADefault", - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{}), - false, - }, - "config overrides default value": { - []string{"strWithDefault"}, - FieldReadResult{ - Value: "fromConfig", - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "strWithDefault": "fromConfig", - }), - false, - }, - "gets default from function when no config set": { - []string{"strWithDefaultFunc"}, - FieldReadResult{ - Value: "FuncDefault", - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{}), - false, - }, - "config overrides default function": { - []string{"strWithDefaultFunc"}, - FieldReadResult{ - Value: "fromConfig", - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "strWithDefaultFunc": "fromConfig", - }), - false, - }, - } - - for name, tc := range cases { - r := &ConfigFieldReader{ - Schema: schema, - Config: tc.Config, - } - out, err := r.ReadField(tc.Addr) - if err != nil != tc.Err { - t.Fatalf("%s: err: %s", name, err) - } - if s, ok := out.Value.(*Set); ok { - // If it is a set, convert to a list so its more easily checked. - out.Value = s.List() - } - if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) - } - } -} - -func TestConfigFieldReader_ComputedMap(t *testing.T) { - schema := map[string]*Schema{ - "map": &Schema{ - Type: TypeMap, - Computed: true, - }, - "listmap": &Schema{ - Type: TypeMap, - Computed: true, - Elem: TypeList, - }, - "maplist": &Schema{ - Type: TypeList, - Computed: true, - Elem: TypeMap, - }, - } - - cases := []struct { - Name string - Addr []string - Result FieldReadResult - Config *terraform.ResourceConfig - Err bool - }{ - { - "set, normal", - []string{"map"}, - FieldReadResult{ - Value: map[string]interface{}{ - "foo": "bar", - }, - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "map": map[string]interface{}{ - "foo": "bar", - }, - }), - false, - }, - - { - "computed element", - []string{"map"}, - FieldReadResult{ - Exists: true, - Computed: true, - }, - testConfig(t, map[string]interface{}{ - "map": map[string]interface{}{ - "foo": hcl2shim.UnknownVariableValue, - }, - }), - false, - }, - - { - "native map", - []string{"map"}, - FieldReadResult{ - Value: map[string]interface{}{ - "bar": "baz", - "baz": "bar", - }, - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "map": map[string]interface{}{ - "bar": "baz", - "baz": "bar", - }, - }), - false, - }, - - { - "map-from-list-of-maps", - []string{"maplist", "0"}, - FieldReadResult{ - Value: map[string]interface{}{ - "key": "bar", - }, - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "maplist": []interface{}{ - map[string]interface{}{ - "key": "bar", - }, - }, - }), - false, - }, - - { - "value-from-list-of-maps", - []string{"maplist", "0", "key"}, - FieldReadResult{ - Value: "bar", - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "maplist": []interface{}{ - map[string]interface{}{ - "key": "bar", - }, - }, - }), - false, - }, - - { - "list-from-map-of-lists", - []string{"listmap", "key"}, - FieldReadResult{ - Value: []interface{}{"bar"}, - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "listmap": map[string]interface{}{ - "key": []interface{}{ - "bar", - }, - }, - }), - false, - }, - - { - "value-from-map-of-lists", - []string{"listmap", "key", "0"}, - FieldReadResult{ - Value: "bar", - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "listmap": map[string]interface{}{ - "key": []interface{}{ - "bar", - }, - }, - }), - false, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - r := &ConfigFieldReader{ - Schema: schema, - Config: tc.Config, - } - out, err := r.ReadField(tc.Addr) - if err != nil != tc.Err { - t.Fatal(err) - } - if s, ok := out.Value.(*Set); ok { - // If it is a set, convert to the raw map - out.Value = s.m - if len(s.m) == 0 { - out.Value = nil - } - } - if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("\nexpected: %#v\ngot: %#v", tc.Result, out) - } - }) - } -} - -func TestConfigFieldReader_ComputedSet(t *testing.T) { - schema := map[string]*Schema{ - "strSet": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeString}, - Set: HashString, - }, - } - - cases := map[string]struct { - Addr []string - Result FieldReadResult - Config *terraform.ResourceConfig - Err bool - }{ - "set, normal": { - []string{"strSet"}, - FieldReadResult{ - Value: map[string]interface{}{ - "2356372769": "foo", - }, - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "strSet": []interface{}{"foo"}, - }), - false, - }, - - "set, computed element": { - []string{"strSet"}, - FieldReadResult{ - Value: nil, - Exists: true, - Computed: true, - }, - testConfig(t, map[string]interface{}{ - "strSet": []interface{}{hcl2shim.UnknownVariableValue}, - }), - false, - }, - } - - for name, tc := range cases { - r := &ConfigFieldReader{ - Schema: schema, - Config: tc.Config, - } - out, err := r.ReadField(tc.Addr) - if err != nil != tc.Err { - t.Fatalf("%s: err: %s", name, err) - } - if s, ok := out.Value.(*Set); ok { - // If it is a set, convert to the raw map - out.Value = s.m - if len(s.m) == 0 { - out.Value = nil - } - } - if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) - } - } -} - -func TestConfigFieldReader_computedComplexSet(t *testing.T) { - hashfunc := func(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["vhd_uri"].(string))) - return hashcode.String(buf.String()) - } - - schema := map[string]*Schema{ - "set": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "name": { - Type: TypeString, - Required: true, - }, - - "vhd_uri": { - Type: TypeString, - Required: true, - }, - }, - }, - Set: hashfunc, - }, - } - - cases := map[string]struct { - Addr []string - Result FieldReadResult - Config *terraform.ResourceConfig - Err bool - }{ - "set, normal": { - []string{"set"}, - FieldReadResult{ - Value: map[string]interface{}{ - "532860136": map[string]interface{}{ - "name": "myosdisk1", - "vhd_uri": "bar", - }, - }, - Exists: true, - Computed: false, - }, - testConfig(t, map[string]interface{}{ - "set": []interface{}{ - map[string]interface{}{ - "name": "myosdisk1", - "vhd_uri": "bar", - }, - }, - }), - false, - }, - } - - for name, tc := range cases { - r := &ConfigFieldReader{ - Schema: schema, - Config: tc.Config, - } - out, err := r.ReadField(tc.Addr) - if err != nil != tc.Err { - t.Fatalf("%s: err: %s", name, err) - } - if s, ok := out.Value.(*Set); ok { - // If it is a set, convert to the raw map - out.Value = s.m - if len(s.m) == 0 { - out.Value = nil - } - } - if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) - } - } -} - -func testConfig(t *testing.T, raw map[string]interface{}) *terraform.ResourceConfig { - return terraform.NewResourceConfigRaw(raw) -} diff --git a/helper/schema/field_reader_diff.go b/helper/schema/field_reader_diff.go deleted file mode 100644 index 3e70acf0b..000000000 --- a/helper/schema/field_reader_diff.go +++ /dev/null @@ -1,244 +0,0 @@ -package schema - -import ( - "fmt" - "strings" - - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/mapstructure" -) - -// DiffFieldReader reads fields out of a diff structures. -// -// It also requires access to a Reader that reads fields from the structure -// that the diff was derived from. This is usually the state. This is required -// because a diff on its own doesn't have complete data about full objects -// such as maps. -// -// The Source MUST be the data that the diff was derived from. If it isn't, -// the behavior of this struct is undefined. -// -// Reading fields from a DiffFieldReader is identical to reading from -// Source except the diff will be applied to the end result. -// -// The "Exists" field on the result will be set to true if the complete -// field exists whether its from the source, diff, or a combination of both. -// It cannot be determined whether a retrieved value is composed of -// diff elements. -type DiffFieldReader struct { - Diff *terraform.InstanceDiff - Source FieldReader - Schema map[string]*Schema - - // cache for memoizing ReadField calls. - cache map[string]cachedFieldReadResult -} - -type cachedFieldReadResult struct { - val FieldReadResult - err error -} - -func (r *DiffFieldReader) ReadField(address []string) (FieldReadResult, error) { - if r.cache == nil { - r.cache = make(map[string]cachedFieldReadResult) - } - - // Create the cache key by joining around a value that isn't a valid part - // of an address. This assumes that the Source and Schema are not changed - // for the life of this DiffFieldReader. - cacheKey := strings.Join(address, "|") - if cached, ok := r.cache[cacheKey]; ok { - return cached.val, cached.err - } - - schemaList := addrToSchema(address, r.Schema) - if len(schemaList) == 0 { - r.cache[cacheKey] = cachedFieldReadResult{} - return FieldReadResult{}, nil - } - - var res FieldReadResult - var err error - - schema := schemaList[len(schemaList)-1] - switch schema.Type { - case TypeBool, TypeInt, TypeFloat, TypeString: - res, err = r.readPrimitive(address, schema) - case TypeList: - res, err = readListField(r, address, schema) - case TypeMap: - res, err = r.readMap(address, schema) - case TypeSet: - res, err = r.readSet(address, schema) - case typeObject: - res, err = readObjectField(r, address, schema.Elem.(map[string]*Schema)) - default: - panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) - } - - r.cache[cacheKey] = cachedFieldReadResult{ - val: res, - err: err, - } - return res, err -} - -func (r *DiffFieldReader) readMap( - address []string, schema *Schema) (FieldReadResult, error) { - result := make(map[string]interface{}) - resultSet := false - - // First read the map from the underlying source - source, err := r.Source.ReadField(address) - if err != nil { - return FieldReadResult{}, err - } - if source.Exists { - // readMap may return a nil value, or an unknown value placeholder in - // some cases, causing the type assertion to panic if we don't assign the ok value - result, _ = source.Value.(map[string]interface{}) - resultSet = true - } - - // Next, read all the elements we have in our diff, and apply - // the diff to our result. - prefix := strings.Join(address, ".") + "." - for k, v := range r.Diff.Attributes { - if !strings.HasPrefix(k, prefix) { - continue - } - if strings.HasPrefix(k, prefix+"%") { - // Ignore the count field - continue - } - - resultSet = true - - k = k[len(prefix):] - if v.NewRemoved { - delete(result, k) - continue - } - - result[k] = v.New - } - - key := address[len(address)-1] - err = mapValuesToPrimitive(key, result, schema) - if err != nil { - return FieldReadResult{}, nil - } - - var resultVal interface{} - if resultSet { - resultVal = result - } - - return FieldReadResult{ - Value: resultVal, - Exists: resultSet, - }, nil -} - -func (r *DiffFieldReader) readPrimitive( - address []string, schema *Schema) (FieldReadResult, error) { - result, err := r.Source.ReadField(address) - if err != nil { - return FieldReadResult{}, err - } - - attrD, ok := r.Diff.Attributes[strings.Join(address, ".")] - if !ok { - return result, nil - } - - var resultVal string - if !attrD.NewComputed { - resultVal = attrD.New - if attrD.NewExtra != nil { - result.ValueProcessed = resultVal - if err := mapstructure.WeakDecode(attrD.NewExtra, &resultVal); err != nil { - return FieldReadResult{}, err - } - } - } - - result.Computed = attrD.NewComputed - result.Exists = true - result.Value, err = stringToPrimitive(resultVal, false, schema) - if err != nil { - return FieldReadResult{}, err - } - - return result, nil -} - -func (r *DiffFieldReader) readSet( - address []string, schema *Schema) (FieldReadResult, error) { - // copy address to ensure we don't modify the argument - address = append([]string(nil), address...) - - prefix := strings.Join(address, ".") + "." - - // Create the set that will be our result - set := schema.ZeroValue().(*Set) - - // Go through the map and find all the set items - for k, d := range r.Diff.Attributes { - if d.NewRemoved { - // If the field is removed, we always ignore it - continue - } - if !strings.HasPrefix(k, prefix) { - continue - } - if strings.HasSuffix(k, "#") { - // Ignore any count field - continue - } - - // Split the key, since it might be a sub-object like "idx.field" - parts := strings.Split(k[len(prefix):], ".") - idx := parts[0] - - raw, err := r.ReadField(append(address, idx)) - if err != nil { - return FieldReadResult{}, err - } - if !raw.Exists { - // This shouldn't happen because we just verified it does exist - panic("missing field in set: " + k + "." + idx) - } - - set.Add(raw.Value) - } - - // Determine if the set "exists". It exists if there are items or if - // the diff explicitly wanted it empty. - exists := set.Len() > 0 - if !exists { - // We could check if the diff value is "0" here but I think the - // existence of "#" on its own is enough to show it existed. This - // protects us in the future from the zero value changing from - // "0" to "" breaking us (if that were to happen). - if _, ok := r.Diff.Attributes[prefix+"#"]; ok { - exists = true - } - } - - if !exists { - result, err := r.Source.ReadField(address) - if err != nil { - return FieldReadResult{}, err - } - if result.Exists { - return result, nil - } - } - - return FieldReadResult{ - Value: set, - Exists: exists, - }, nil -} diff --git a/helper/schema/field_reader_diff_test.go b/helper/schema/field_reader_diff_test.go deleted file mode 100644 index 49b05e862..000000000 --- a/helper/schema/field_reader_diff_test.go +++ /dev/null @@ -1,524 +0,0 @@ -package schema - -import ( - "reflect" - "testing" - - "github.com/hashicorp/terraform/terraform" -) - -func TestDiffFieldReader_impl(t *testing.T) { - var _ FieldReader = new(DiffFieldReader) -} - -func TestDiffFieldReader_NestedSetUpdate(t *testing.T) { - hashFn := func(a interface{}) int { - m := a.(map[string]interface{}) - return m["val"].(int) - } - - schema := map[string]*Schema{ - "list_of_sets_1": &Schema{ - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "nested_set": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "val": &Schema{ - Type: TypeInt, - }, - }, - }, - Set: hashFn, - }, - }, - }, - }, - "list_of_sets_2": &Schema{ - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "nested_set": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "val": &Schema{ - Type: TypeInt, - }, - }, - }, - Set: hashFn, - }, - }, - }, - }, - } - - r := &DiffFieldReader{ - Schema: schema, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "list_of_sets_1.0.nested_set.1.val": &terraform.ResourceAttrDiff{ - Old: "1", - New: "0", - NewRemoved: true, - }, - "list_of_sets_1.0.nested_set.2.val": &terraform.ResourceAttrDiff{ - New: "2", - }, - }, - }, - } - - r.Source = &MultiLevelFieldReader{ - Readers: map[string]FieldReader{ - "diff": r, - "set": &MapFieldReader{Schema: schema}, - "state": &MapFieldReader{ - Map: &BasicMapReader{ - "list_of_sets_1.#": "1", - "list_of_sets_1.0.nested_set.#": "1", - "list_of_sets_1.0.nested_set.1.val": "1", - "list_of_sets_2.#": "1", - "list_of_sets_2.0.nested_set.#": "1", - "list_of_sets_2.0.nested_set.1.val": "1", - }, - Schema: schema, - }, - }, - Levels: []string{"state", "config"}, - } - - out, err := r.ReadField([]string{"list_of_sets_2"}) - if err != nil { - t.Fatalf("err: %v", err) - } - - s := &Set{F: hashFn} - s.Add(map[string]interface{}{"val": 1}) - expected := s.List() - - l := out.Value.([]interface{}) - i := l[0].(map[string]interface{}) - actual := i["nested_set"].(*Set).List() - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("bad: NestedSetUpdate\n\nexpected: %#v\n\ngot: %#v\n\n", expected, actual) - } -} - -// https://github.com/hashicorp/terraform/issues/914 -func TestDiffFieldReader_MapHandling(t *testing.T) { - schema := map[string]*Schema{ - "tags": &Schema{ - Type: TypeMap, - }, - } - r := &DiffFieldReader{ - Schema: schema, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "tags.%": &terraform.ResourceAttrDiff{ - Old: "1", - New: "2", - }, - "tags.baz": &terraform.ResourceAttrDiff{ - Old: "", - New: "qux", - }, - }, - }, - Source: &MapFieldReader{ - Schema: schema, - Map: BasicMapReader(map[string]string{ - "tags.%": "1", - "tags.foo": "bar", - }), - }, - } - - result, err := r.ReadField([]string{"tags"}) - if err != nil { - t.Fatalf("ReadField failed: %#v", err) - } - - expected := map[string]interface{}{ - "foo": "bar", - "baz": "qux", - } - - if !reflect.DeepEqual(expected, result.Value) { - t.Fatalf("bad: DiffHandling\n\nexpected: %#v\n\ngot: %#v\n\n", expected, result.Value) - } -} - -func TestDiffFieldReader_extra(t *testing.T) { - schema := map[string]*Schema{ - "stringComputed": &Schema{Type: TypeString}, - - "listMap": &Schema{ - Type: TypeList, - Elem: &Schema{ - Type: TypeMap, - }, - }, - - "mapRemove": &Schema{Type: TypeMap}, - - "setChange": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "value": &Schema{ - Type: TypeString, - Required: true, - }, - }, - }, - Set: func(a interface{}) int { - m := a.(map[string]interface{}) - return m["index"].(int) - }, - }, - - "setEmpty": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "value": &Schema{ - Type: TypeString, - Required: true, - }, - }, - }, - Set: func(a interface{}) int { - m := a.(map[string]interface{}) - return m["index"].(int) - }, - }, - } - - r := &DiffFieldReader{ - Schema: schema, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "stringComputed": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - NewComputed: true, - }, - - "listMap.0.bar": &terraform.ResourceAttrDiff{ - NewRemoved: true, - }, - - "mapRemove.bar": &terraform.ResourceAttrDiff{ - NewRemoved: true, - }, - - "setChange.10.value": &terraform.ResourceAttrDiff{ - Old: "50", - New: "80", - }, - - "setEmpty.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "0", - }, - }, - }, - - Source: &MapFieldReader{ - Schema: schema, - Map: BasicMapReader(map[string]string{ - "listMap.#": "2", - "listMap.0.foo": "bar", - "listMap.0.bar": "baz", - "listMap.1.baz": "baz", - - "mapRemove.foo": "bar", - "mapRemove.bar": "bar", - - "setChange.#": "1", - "setChange.10.index": "10", - "setChange.10.value": "50", - - "setEmpty.#": "2", - "setEmpty.10.index": "10", - "setEmpty.10.value": "50", - "setEmpty.20.index": "20", - "setEmpty.20.value": "50", - }), - }, - } - - cases := map[string]struct { - Addr []string - Result FieldReadResult - Err bool - }{ - "stringComputed": { - []string{"stringComputed"}, - FieldReadResult{ - Value: "", - Exists: true, - Computed: true, - }, - false, - }, - - "listMapRemoval": { - []string{"listMap"}, - FieldReadResult{ - Value: []interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - map[string]interface{}{ - "baz": "baz", - }, - }, - Exists: true, - }, - false, - }, - - "mapRemove": { - []string{"mapRemove"}, - FieldReadResult{ - Value: map[string]interface{}{ - "foo": "bar", - }, - Exists: true, - Computed: false, - }, - false, - }, - - "setChange": { - []string{"setChange"}, - FieldReadResult{ - Value: []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "80", - }, - }, - Exists: true, - }, - false, - }, - - "setEmpty": { - []string{"setEmpty"}, - FieldReadResult{ - Value: []interface{}{}, - Exists: true, - }, - false, - }, - } - - for name, tc := range cases { - out, err := r.ReadField(tc.Addr) - if err != nil != tc.Err { - t.Fatalf("%s: err: %s", name, err) - } - if s, ok := out.Value.(*Set); ok { - // If it is a set, convert to a list so its more easily checked. - out.Value = s.List() - } - if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) - } - } -} - -func TestDiffFieldReader(t *testing.T) { - testFieldReader(t, func(s map[string]*Schema) FieldReader { - return &DiffFieldReader{ - Schema: s, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "bool": &terraform.ResourceAttrDiff{ - Old: "", - New: "true", - }, - - "int": &terraform.ResourceAttrDiff{ - Old: "", - New: "42", - }, - - "float": &terraform.ResourceAttrDiff{ - Old: "", - New: "3.1415", - }, - - "string": &terraform.ResourceAttrDiff{ - Old: "", - New: "string", - }, - - "stringComputed": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - NewComputed: true, - }, - - "list.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "2", - }, - - "list.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - - "list.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - - "listInt.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "2", - }, - - "listInt.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "21", - }, - - "listInt.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "42", - }, - - "map.foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - - "map.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - - "mapInt.%": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "mapInt.one": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "mapInt.two": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - - "mapIntNestedSchema.%": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "mapIntNestedSchema.one": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "mapIntNestedSchema.two": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - - "mapFloat.%": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "mapFloat.oneDotTwo": &terraform.ResourceAttrDiff{ - Old: "", - New: "1.2", - }, - - "mapBool.%": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "mapBool.True": &terraform.ResourceAttrDiff{ - Old: "", - New: "true", - }, - "mapBool.False": &terraform.ResourceAttrDiff{ - Old: "", - New: "false", - }, - - "set.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "2", - }, - - "set.10": &terraform.ResourceAttrDiff{ - Old: "", - New: "10", - }, - - "set.50": &terraform.ResourceAttrDiff{ - Old: "", - New: "50", - }, - - "setDeep.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "2", - }, - - "setDeep.10.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "10", - }, - - "setDeep.10.value": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - - "setDeep.50.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "50", - }, - - "setDeep.50.value": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - - Source: &MapFieldReader{ - Schema: s, - Map: BasicMapReader(map[string]string{ - "listMap.#": "2", - "listMap.0.foo": "bar", - "listMap.0.bar": "baz", - "listMap.1.baz": "baz", - }), - }, - } - }) -} diff --git a/helper/schema/field_reader_map.go b/helper/schema/field_reader_map.go deleted file mode 100644 index 53f73b71b..000000000 --- a/helper/schema/field_reader_map.go +++ /dev/null @@ -1,235 +0,0 @@ -package schema - -import ( - "fmt" - "strings" -) - -// MapFieldReader reads fields out of an untyped map[string]string to -// the best of its ability. -type MapFieldReader struct { - Map MapReader - Schema map[string]*Schema -} - -func (r *MapFieldReader) ReadField(address []string) (FieldReadResult, error) { - k := strings.Join(address, ".") - schemaList := addrToSchema(address, r.Schema) - if len(schemaList) == 0 { - return FieldReadResult{}, nil - } - - schema := schemaList[len(schemaList)-1] - switch schema.Type { - case TypeBool, TypeInt, TypeFloat, TypeString: - return r.readPrimitive(address, schema) - case TypeList: - return readListField(r, address, schema) - case TypeMap: - return r.readMap(k, schema) - case TypeSet: - return r.readSet(address, schema) - case typeObject: - return readObjectField(r, address, schema.Elem.(map[string]*Schema)) - default: - panic(fmt.Sprintf("Unknown type: %s", schema.Type)) - } -} - -func (r *MapFieldReader) readMap(k string, schema *Schema) (FieldReadResult, error) { - result := make(map[string]interface{}) - resultSet := false - - // If the name of the map field is directly in the map with an - // empty string, it means that the map is being deleted, so mark - // that is is set. - if v, ok := r.Map.Access(k); ok && v == "" { - resultSet = true - } - - prefix := k + "." - r.Map.Range(func(k, v string) bool { - if strings.HasPrefix(k, prefix) { - resultSet = true - - key := k[len(prefix):] - if key != "%" && key != "#" { - result[key] = v - } - } - - return true - }) - - err := mapValuesToPrimitive(k, result, schema) - if err != nil { - return FieldReadResult{}, nil - } - - var resultVal interface{} - if resultSet { - resultVal = result - } - - return FieldReadResult{ - Value: resultVal, - Exists: resultSet, - }, nil -} - -func (r *MapFieldReader) readPrimitive( - address []string, schema *Schema) (FieldReadResult, error) { - k := strings.Join(address, ".") - result, ok := r.Map.Access(k) - if !ok { - return FieldReadResult{}, nil - } - - returnVal, err := stringToPrimitive(result, false, schema) - if err != nil { - return FieldReadResult{}, err - } - - return FieldReadResult{ - Value: returnVal, - Exists: true, - }, nil -} - -func (r *MapFieldReader) readSet( - address []string, schema *Schema) (FieldReadResult, error) { - // copy address to ensure we don't modify the argument - address = append([]string(nil), address...) - - // Get the number of elements in the list - countRaw, err := r.readPrimitive( - append(address, "#"), &Schema{Type: TypeInt}) - if err != nil { - return FieldReadResult{}, err - } - if !countRaw.Exists { - // No count, means we have no list - countRaw.Value = 0 - } - - // Create the set that will be our result - set := schema.ZeroValue().(*Set) - - // If we have an empty list, then return an empty list - if countRaw.Computed || countRaw.Value.(int) == 0 { - return FieldReadResult{ - Value: set, - Exists: countRaw.Exists, - Computed: countRaw.Computed, - }, nil - } - - // Go through the map and find all the set items - prefix := strings.Join(address, ".") + "." - countExpected := countRaw.Value.(int) - countActual := make(map[string]struct{}) - completed := r.Map.Range(func(k, _ string) bool { - if !strings.HasPrefix(k, prefix) { - return true - } - if strings.HasPrefix(k, prefix+"#") { - // Ignore the count field - return true - } - - // Split the key, since it might be a sub-object like "idx.field" - parts := strings.Split(k[len(prefix):], ".") - idx := parts[0] - - var raw FieldReadResult - raw, err = r.ReadField(append(address, idx)) - if err != nil { - return false - } - if !raw.Exists { - // This shouldn't happen because we just verified it does exist - panic("missing field in set: " + k + "." + idx) - } - - set.Add(raw.Value) - - // Due to the way multimap readers work, if we've seen the number - // of fields we expect, then exit so that we don't read later values. - // For example: the "set" map might have "ports.#", "ports.0", and - // "ports.1", but the "state" map might have those plus "ports.2". - // We don't want "ports.2" - countActual[idx] = struct{}{} - if len(countActual) >= countExpected { - return false - } - - return true - }) - if !completed && err != nil { - return FieldReadResult{}, err - } - - return FieldReadResult{ - Value: set, - Exists: true, - }, nil -} - -// MapReader is an interface that is given to MapFieldReader for accessing -// a "map". This can be used to have alternate implementations. For a basic -// map[string]string, use BasicMapReader. -type MapReader interface { - Access(string) (string, bool) - Range(func(string, string) bool) bool -} - -// BasicMapReader implements MapReader for a single map. -type BasicMapReader map[string]string - -func (r BasicMapReader) Access(k string) (string, bool) { - v, ok := r[k] - return v, ok -} - -func (r BasicMapReader) Range(f func(string, string) bool) bool { - for k, v := range r { - if cont := f(k, v); !cont { - return false - } - } - - return true -} - -// MultiMapReader reads over multiple maps, preferring keys that are -// founder earlier (lower number index) vs. later (higher number index) -type MultiMapReader []map[string]string - -func (r MultiMapReader) Access(k string) (string, bool) { - for _, m := range r { - if v, ok := m[k]; ok { - return v, ok - } - } - - return "", false -} - -func (r MultiMapReader) Range(f func(string, string) bool) bool { - done := make(map[string]struct{}) - for _, m := range r { - for k, v := range m { - if _, ok := done[k]; ok { - continue - } - - if cont := f(k, v); !cont { - return false - } - - done[k] = struct{}{} - } - } - - return true -} diff --git a/helper/schema/field_reader_map_test.go b/helper/schema/field_reader_map_test.go deleted file mode 100644 index 2723674a3..000000000 --- a/helper/schema/field_reader_map_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package schema - -import ( - "reflect" - "testing" -) - -func TestMapFieldReader_impl(t *testing.T) { - var _ FieldReader = new(MapFieldReader) -} - -func TestMapFieldReader(t *testing.T) { - testFieldReader(t, func(s map[string]*Schema) FieldReader { - return &MapFieldReader{ - Schema: s, - - Map: BasicMapReader(map[string]string{ - "bool": "true", - "int": "42", - "float": "3.1415", - "string": "string", - - "list.#": "2", - "list.0": "foo", - "list.1": "bar", - - "listInt.#": "2", - "listInt.0": "21", - "listInt.1": "42", - - "map.%": "2", - "map.foo": "bar", - "map.bar": "baz", - - "set.#": "2", - "set.10": "10", - "set.50": "50", - - "setDeep.#": "2", - "setDeep.10.index": "10", - "setDeep.10.value": "foo", - "setDeep.50.index": "50", - "setDeep.50.value": "bar", - - "mapInt.%": "2", - "mapInt.one": "1", - "mapInt.two": "2", - - "mapIntNestedSchema.%": "2", - "mapIntNestedSchema.one": "1", - "mapIntNestedSchema.two": "2", - - "mapFloat.%": "1", - "mapFloat.oneDotTwo": "1.2", - - "mapBool.%": "2", - "mapBool.True": "true", - "mapBool.False": "false", - }), - } - }) -} - -func TestMapFieldReader_extra(t *testing.T) { - r := &MapFieldReader{ - Schema: map[string]*Schema{ - "mapDel": &Schema{Type: TypeMap}, - "mapEmpty": &Schema{Type: TypeMap}, - }, - - Map: BasicMapReader(map[string]string{ - "mapDel": "", - - "mapEmpty.%": "0", - }), - } - - cases := map[string]struct { - Addr []string - Out interface{} - OutOk bool - OutComputed bool - OutErr bool - }{ - "mapDel": { - []string{"mapDel"}, - map[string]interface{}{}, - true, - false, - false, - }, - - "mapEmpty": { - []string{"mapEmpty"}, - map[string]interface{}{}, - true, - false, - false, - }, - } - - for name, tc := range cases { - out, err := r.ReadField(tc.Addr) - if err != nil != tc.OutErr { - t.Fatalf("%s: err: %s", name, err) - } - if out.Computed != tc.OutComputed { - t.Fatalf("%s: err: %#v", name, out.Computed) - } - - if s, ok := out.Value.(*Set); ok { - // If it is a set, convert to a list so its more easily checked. - out.Value = s.List() - } - - if !reflect.DeepEqual(out.Value, tc.Out) { - t.Fatalf("%s: out: %#v", name, out.Value) - } - if out.Exists != tc.OutOk { - t.Fatalf("%s: outOk: %#v", name, out.Exists) - } - } -} diff --git a/helper/schema/field_reader_multi.go b/helper/schema/field_reader_multi.go deleted file mode 100644 index 89ad3a86f..000000000 --- a/helper/schema/field_reader_multi.go +++ /dev/null @@ -1,63 +0,0 @@ -package schema - -import ( - "fmt" -) - -// MultiLevelFieldReader reads from other field readers, -// merging their results along the way in a specific order. You can specify -// "levels" and name them in order to read only an exact level or up to -// a specific level. -// -// This is useful for saying things such as "read the field from the state -// and config and merge them" or "read the latest value of the field". -type MultiLevelFieldReader struct { - Readers map[string]FieldReader - Levels []string -} - -func (r *MultiLevelFieldReader) ReadField(address []string) (FieldReadResult, error) { - return r.ReadFieldMerge(address, r.Levels[len(r.Levels)-1]) -} - -func (r *MultiLevelFieldReader) ReadFieldExact( - address []string, level string) (FieldReadResult, error) { - reader, ok := r.Readers[level] - if !ok { - return FieldReadResult{}, fmt.Errorf( - "Unknown reader level: %s", level) - } - - result, err := reader.ReadField(address) - if err != nil { - return FieldReadResult{}, fmt.Errorf( - "Error reading level %s: %s", level, err) - } - - return result, nil -} - -func (r *MultiLevelFieldReader) ReadFieldMerge( - address []string, level string) (FieldReadResult, error) { - var result FieldReadResult - for _, l := range r.Levels { - if r, ok := r.Readers[l]; ok { - out, err := r.ReadField(address) - if err != nil { - return FieldReadResult{}, fmt.Errorf( - "Error reading level %s: %s", l, err) - } - - // TODO: computed - if out.Exists { - result = out - } - } - - if l == level { - break - } - } - - return result, nil -} diff --git a/helper/schema/field_reader_multi_test.go b/helper/schema/field_reader_multi_test.go deleted file mode 100644 index 85286a66e..000000000 --- a/helper/schema/field_reader_multi_test.go +++ /dev/null @@ -1,270 +0,0 @@ -package schema - -import ( - "reflect" - "strconv" - "testing" - - "github.com/hashicorp/terraform/terraform" -) - -func TestMultiLevelFieldReaderReadFieldExact(t *testing.T) { - cases := map[string]struct { - Addr []string - Readers []FieldReader - Level string - Result FieldReadResult - }{ - "specific": { - Addr: []string{"foo"}, - - Readers: []FieldReader{ - &MapFieldReader{ - Schema: map[string]*Schema{ - "foo": &Schema{Type: TypeString}, - }, - Map: BasicMapReader(map[string]string{ - "foo": "bar", - }), - }, - &MapFieldReader{ - Schema: map[string]*Schema{ - "foo": &Schema{Type: TypeString}, - }, - Map: BasicMapReader(map[string]string{ - "foo": "baz", - }), - }, - &MapFieldReader{ - Schema: map[string]*Schema{ - "foo": &Schema{Type: TypeString}, - }, - Map: BasicMapReader(map[string]string{}), - }, - }, - - Level: "1", - Result: FieldReadResult{ - Value: "baz", - Exists: true, - }, - }, - } - - for name, tc := range cases { - readers := make(map[string]FieldReader) - levels := make([]string, len(tc.Readers)) - for i, r := range tc.Readers { - is := strconv.FormatInt(int64(i), 10) - readers[is] = r - levels[i] = is - } - - r := &MultiLevelFieldReader{ - Readers: readers, - Levels: levels, - } - - out, err := r.ReadFieldExact(tc.Addr, tc.Level) - if err != nil { - t.Fatalf("%s: err: %s", name, err) - } - - if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) - } - } -} - -func TestMultiLevelFieldReaderReadFieldMerge(t *testing.T) { - cases := map[string]struct { - Addr []string - Readers []FieldReader - Result FieldReadResult - }{ - "stringInDiff": { - Addr: []string{"availability_zone"}, - - Readers: []FieldReader{ - &DiffFieldReader{ - Schema: map[string]*Schema{ - "availability_zone": &Schema{Type: TypeString}, - }, - - Source: &MapFieldReader{ - Schema: map[string]*Schema{ - "availability_zone": &Schema{Type: TypeString}, - }, - Map: BasicMapReader(map[string]string{ - "availability_zone": "foo", - }), - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - RequiresNew: true, - }, - }, - }, - }, - }, - - Result: FieldReadResult{ - Value: "bar", - Exists: true, - }, - }, - - "lastLevelComputed": { - Addr: []string{"availability_zone"}, - - Readers: []FieldReader{ - &MapFieldReader{ - Schema: map[string]*Schema{ - "availability_zone": &Schema{Type: TypeString}, - }, - - Map: BasicMapReader(map[string]string{ - "availability_zone": "foo", - }), - }, - - &DiffFieldReader{ - Schema: map[string]*Schema{ - "availability_zone": &Schema{Type: TypeString}, - }, - - Source: &MapFieldReader{ - Schema: map[string]*Schema{ - "availability_zone": &Schema{Type: TypeString}, - }, - - Map: BasicMapReader(map[string]string{ - "availability_zone": "foo", - }), - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - NewComputed: true, - }, - }, - }, - }, - }, - - Result: FieldReadResult{ - Value: "", - Exists: true, - Computed: true, - }, - }, - - "list of maps with removal in diff": { - Addr: []string{"config_vars"}, - - Readers: []FieldReader{ - &DiffFieldReader{ - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeMap}, - }, - }, - - Source: &MapFieldReader{ - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeMap}, - }, - }, - - Map: BasicMapReader(map[string]string{ - "config_vars.#": "2", - "config_vars.0.foo": "bar", - "config_vars.0.bar": "bar", - "config_vars.1.bar": "baz", - }), - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.0.bar": &terraform.ResourceAttrDiff{ - NewRemoved: true, - }, - }, - }, - }, - }, - - Result: FieldReadResult{ - Value: []interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - map[string]interface{}{ - "bar": "baz", - }, - }, - Exists: true, - }, - }, - - "first level only": { - Addr: []string{"foo"}, - - Readers: []FieldReader{ - &MapFieldReader{ - Schema: map[string]*Schema{ - "foo": &Schema{Type: TypeString}, - }, - Map: BasicMapReader(map[string]string{ - "foo": "bar", - }), - }, - &MapFieldReader{ - Schema: map[string]*Schema{ - "foo": &Schema{Type: TypeString}, - }, - Map: BasicMapReader(map[string]string{}), - }, - }, - - Result: FieldReadResult{ - Value: "bar", - Exists: true, - }, - }, - } - - for name, tc := range cases { - readers := make(map[string]FieldReader) - levels := make([]string, len(tc.Readers)) - for i, r := range tc.Readers { - is := strconv.FormatInt(int64(i), 10) - readers[is] = r - levels[i] = is - } - - r := &MultiLevelFieldReader{ - Readers: readers, - Levels: levels, - } - - out, err := r.ReadFieldMerge(tc.Addr, levels[len(levels)-1]) - if err != nil { - t.Fatalf("%s: err: %s", name, err) - } - - if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) - } - } -} diff --git a/helper/schema/field_reader_test.go b/helper/schema/field_reader_test.go deleted file mode 100644 index 2c62eb0a8..000000000 --- a/helper/schema/field_reader_test.go +++ /dev/null @@ -1,471 +0,0 @@ -package schema - -import ( - "reflect" - "testing" -) - -func TestAddrToSchema(t *testing.T) { - cases := map[string]struct { - Addr []string - Schema map[string]*Schema - Result []ValueType - }{ - "full object": { - []string{}, - map[string]*Schema{ - "list": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - }, - []ValueType{typeObject}, - }, - - "list": { - []string{"list"}, - map[string]*Schema{ - "list": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - }, - []ValueType{TypeList}, - }, - - "list.#": { - []string{"list", "#"}, - map[string]*Schema{ - "list": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - }, - []ValueType{TypeList, TypeInt}, - }, - - "list.0": { - []string{"list", "0"}, - map[string]*Schema{ - "list": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - }, - []ValueType{TypeList, TypeInt}, - }, - - "list.0 with resource": { - []string{"list", "0"}, - map[string]*Schema{ - "list": &Schema{ - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "field": &Schema{Type: TypeString}, - }, - }, - }, - }, - []ValueType{TypeList, typeObject}, - }, - - "list.0.field": { - []string{"list", "0", "field"}, - map[string]*Schema{ - "list": &Schema{ - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "field": &Schema{Type: TypeString}, - }, - }, - }, - }, - []ValueType{TypeList, typeObject, TypeString}, - }, - - "set": { - []string{"set"}, - map[string]*Schema{ - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - []ValueType{TypeSet}, - }, - - "set.#": { - []string{"set", "#"}, - map[string]*Schema{ - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - []ValueType{TypeSet, TypeInt}, - }, - - "set.0": { - []string{"set", "0"}, - map[string]*Schema{ - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - []ValueType{TypeSet, TypeInt}, - }, - - "set.0 with resource": { - []string{"set", "0"}, - map[string]*Schema{ - "set": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "field": &Schema{Type: TypeString}, - }, - }, - }, - }, - []ValueType{TypeSet, typeObject}, - }, - - "mapElem": { - []string{"map", "foo"}, - map[string]*Schema{ - "map": &Schema{Type: TypeMap}, - }, - []ValueType{TypeMap, TypeString}, - }, - - "setDeep": { - []string{"set", "50", "index"}, - map[string]*Schema{ - "set": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, - }, - []ValueType{TypeSet, typeObject, TypeInt}, - }, - } - - for name, tc := range cases { - result := addrToSchema(tc.Addr, tc.Schema) - types := make([]ValueType, len(result)) - for i, v := range result { - types[i] = v.Type - } - - if !reflect.DeepEqual(types, tc.Result) { - t.Fatalf("%s: %#v", name, types) - } - } -} - -// testFieldReader is a helper that should be used to verify that -// a FieldReader behaves properly in all the common cases. -func testFieldReader(t *testing.T, f func(map[string]*Schema) FieldReader) { - schema := map[string]*Schema{ - // Primitives - "bool": &Schema{Type: TypeBool}, - "float": &Schema{Type: TypeFloat}, - "int": &Schema{Type: TypeInt}, - "string": &Schema{Type: TypeString}, - - // Lists - "list": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeString}, - }, - "listInt": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - "listMap": &Schema{ - Type: TypeList, - Elem: &Schema{ - Type: TypeMap, - }, - }, - - // Maps - "map": &Schema{Type: TypeMap}, - "mapInt": &Schema{ - Type: TypeMap, - Elem: TypeInt, - }, - - // This is used to verify that the type of a Map can be specified using the - // same syntax as for lists (as a nested *Schema passed to Elem) - "mapIntNestedSchema": &Schema{ - Type: TypeMap, - Elem: &Schema{Type: TypeInt}, - }, - "mapFloat": &Schema{ - Type: TypeMap, - Elem: TypeFloat, - }, - "mapBool": &Schema{ - Type: TypeMap, - Elem: TypeBool, - }, - - // Sets - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - "setDeep": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, - "setEmpty": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - } - - cases := map[string]struct { - Addr []string - Result FieldReadResult - Err bool - }{ - "noexist": { - []string{"boolNOPE"}, - FieldReadResult{ - Value: nil, - Exists: false, - Computed: false, - }, - false, - }, - - "bool": { - []string{"bool"}, - FieldReadResult{ - Value: true, - Exists: true, - Computed: false, - }, - false, - }, - - "float": { - []string{"float"}, - FieldReadResult{ - Value: 3.1415, - Exists: true, - Computed: false, - }, - false, - }, - - "int": { - []string{"int"}, - FieldReadResult{ - Value: 42, - Exists: true, - Computed: false, - }, - false, - }, - - "string": { - []string{"string"}, - FieldReadResult{ - Value: "string", - Exists: true, - Computed: false, - }, - false, - }, - - "list": { - []string{"list"}, - FieldReadResult{ - Value: []interface{}{ - "foo", - "bar", - }, - Exists: true, - Computed: false, - }, - false, - }, - - "listInt": { - []string{"listInt"}, - FieldReadResult{ - Value: []interface{}{ - 21, - 42, - }, - Exists: true, - Computed: false, - }, - false, - }, - - "map": { - []string{"map"}, - FieldReadResult{ - Value: map[string]interface{}{ - "foo": "bar", - "bar": "baz", - }, - Exists: true, - Computed: false, - }, - false, - }, - - "mapInt": { - []string{"mapInt"}, - FieldReadResult{ - Value: map[string]interface{}{ - "one": 1, - "two": 2, - }, - Exists: true, - Computed: false, - }, - false, - }, - - "mapIntNestedSchema": { - []string{"mapIntNestedSchema"}, - FieldReadResult{ - Value: map[string]interface{}{ - "one": 1, - "two": 2, - }, - Exists: true, - Computed: false, - }, - false, - }, - - "mapFloat": { - []string{"mapFloat"}, - FieldReadResult{ - Value: map[string]interface{}{ - "oneDotTwo": 1.2, - }, - Exists: true, - Computed: false, - }, - false, - }, - - "mapBool": { - []string{"mapBool"}, - FieldReadResult{ - Value: map[string]interface{}{ - "True": true, - "False": false, - }, - Exists: true, - Computed: false, - }, - false, - }, - - "mapelem": { - []string{"map", "foo"}, - FieldReadResult{ - Value: "bar", - Exists: true, - Computed: false, - }, - false, - }, - - "set": { - []string{"set"}, - FieldReadResult{ - Value: []interface{}{10, 50}, - Exists: true, - Computed: false, - }, - false, - }, - - "setDeep": { - []string{"setDeep"}, - FieldReadResult{ - Value: []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "foo", - }, - map[string]interface{}{ - "index": 50, - "value": "bar", - }, - }, - Exists: true, - Computed: false, - }, - false, - }, - - "setEmpty": { - []string{"setEmpty"}, - FieldReadResult{ - Value: []interface{}{}, - Exists: false, - }, - false, - }, - } - - for name, tc := range cases { - r := f(schema) - out, err := r.ReadField(tc.Addr) - if err != nil != tc.Err { - t.Fatalf("%s: err: %s", name, err) - } - if s, ok := out.Value.(*Set); ok { - // If it is a set, convert to a list so its more easily checked. - out.Value = s.List() - } - if !reflect.DeepEqual(tc.Result, out) { - t.Fatalf("%s: bad: %#v", name, out) - } - } -} diff --git a/helper/schema/field_writer.go b/helper/schema/field_writer.go deleted file mode 100644 index 9abc41b54..000000000 --- a/helper/schema/field_writer.go +++ /dev/null @@ -1,8 +0,0 @@ -package schema - -// FieldWriters are responsible for writing fields by address into -// a proper typed representation. ResourceData uses this to write new data -// into existing sources. -type FieldWriter interface { - WriteField([]string, interface{}) error -} diff --git a/helper/schema/field_writer_map.go b/helper/schema/field_writer_map.go deleted file mode 100644 index fca3bab0a..000000000 --- a/helper/schema/field_writer_map.go +++ /dev/null @@ -1,357 +0,0 @@ -package schema - -import ( - "fmt" - "reflect" - "strconv" - "strings" - "sync" - - "github.com/mitchellh/mapstructure" -) - -// MapFieldWriter writes data into a single map[string]string structure. -type MapFieldWriter struct { - Schema map[string]*Schema - - lock sync.Mutex - result map[string]string -} - -// Map returns the underlying map that is being written to. -func (w *MapFieldWriter) Map() map[string]string { - w.lock.Lock() - defer w.lock.Unlock() - if w.result == nil { - w.result = make(map[string]string) - } - - return w.result -} - -func (w *MapFieldWriter) unsafeWriteField(addr string, value string) { - w.lock.Lock() - defer w.lock.Unlock() - if w.result == nil { - w.result = make(map[string]string) - } - - w.result[addr] = value -} - -// clearTree clears a field and any sub-fields of the given address out of the -// map. This should be used to reset some kind of complex structures (namely -// sets) before writing to make sure that any conflicting data is removed (for -// example, if the set was previously written to the writer's layer). -func (w *MapFieldWriter) clearTree(addr []string) { - prefix := strings.Join(addr, ".") + "." - for k := range w.result { - if strings.HasPrefix(k, prefix) { - delete(w.result, k) - } - } -} - -func (w *MapFieldWriter) WriteField(addr []string, value interface{}) error { - w.lock.Lock() - defer w.lock.Unlock() - if w.result == nil { - w.result = make(map[string]string) - } - - schemaList := addrToSchema(addr, w.Schema) - if len(schemaList) == 0 { - return fmt.Errorf("Invalid address to set: %#v", addr) - } - - // If we're setting anything other than a list root or set root, - // then disallow it. - for _, schema := range schemaList[:len(schemaList)-1] { - if schema.Type == TypeList { - return fmt.Errorf( - "%s: can only set full list", - strings.Join(addr, ".")) - } - - if schema.Type == TypeMap { - return fmt.Errorf( - "%s: can only set full map", - strings.Join(addr, ".")) - } - - if schema.Type == TypeSet { - return fmt.Errorf( - "%s: can only set full set", - strings.Join(addr, ".")) - } - } - - return w.set(addr, value) -} - -func (w *MapFieldWriter) set(addr []string, value interface{}) error { - schemaList := addrToSchema(addr, w.Schema) - if len(schemaList) == 0 { - return fmt.Errorf("Invalid address to set: %#v", addr) - } - - schema := schemaList[len(schemaList)-1] - switch schema.Type { - case TypeBool, TypeInt, TypeFloat, TypeString: - return w.setPrimitive(addr, value, schema) - case TypeList: - return w.setList(addr, value, schema) - case TypeMap: - return w.setMap(addr, value, schema) - case TypeSet: - return w.setSet(addr, value, schema) - case typeObject: - return w.setObject(addr, value, schema) - default: - panic(fmt.Sprintf("Unknown type: %#v", schema.Type)) - } -} - -func (w *MapFieldWriter) setList( - addr []string, - v interface{}, - schema *Schema) error { - k := strings.Join(addr, ".") - setElement := func(idx string, value interface{}) error { - addrCopy := make([]string, len(addr), len(addr)+1) - copy(addrCopy, addr) - return w.set(append(addrCopy, idx), value) - } - - var vs []interface{} - if err := mapstructure.Decode(v, &vs); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - // Wipe the set from the current writer prior to writing if it exists. - // Multiple writes to the same layer is a lot safer for lists than sets due - // to the fact that indexes are always deterministic and the length will - // always be updated with the current length on the last write, but making - // sure we have a clean namespace removes any chance for edge cases to pop up - // and ensures that the last write to the set is the correct value. - w.clearTree(addr) - - // Set the entire list. - var err error - for i, elem := range vs { - is := strconv.FormatInt(int64(i), 10) - err = setElement(is, elem) - if err != nil { - break - } - } - if err != nil { - for i, _ := range vs { - is := strconv.FormatInt(int64(i), 10) - setElement(is, nil) - } - - return err - } - - w.result[k+".#"] = strconv.FormatInt(int64(len(vs)), 10) - return nil -} - -func (w *MapFieldWriter) setMap( - addr []string, - value interface{}, - schema *Schema) error { - k := strings.Join(addr, ".") - v := reflect.ValueOf(value) - vs := make(map[string]interface{}) - - if value == nil { - // The empty string here means the map is removed. - w.result[k] = "" - return nil - } - - if v.Kind() != reflect.Map { - return fmt.Errorf("%s: must be a map", k) - } - if v.Type().Key().Kind() != reflect.String { - return fmt.Errorf("%s: keys must strings", k) - } - for _, mk := range v.MapKeys() { - mv := v.MapIndex(mk) - vs[mk.String()] = mv.Interface() - } - - // Wipe this address tree. The contents of the map should always reflect the - // last write made to it. - w.clearTree(addr) - - // Remove the pure key since we're setting the full map value - delete(w.result, k) - - // Set each subkey - addrCopy := make([]string, len(addr), len(addr)+1) - copy(addrCopy, addr) - for subKey, v := range vs { - if err := w.set(append(addrCopy, subKey), v); err != nil { - return err - } - } - - // Set the count - w.result[k+".%"] = strconv.Itoa(len(vs)) - - return nil -} - -func (w *MapFieldWriter) setObject( - addr []string, - value interface{}, - schema *Schema) error { - // Set the entire object. First decode into a proper structure - var v map[string]interface{} - if err := mapstructure.Decode(value, &v); err != nil { - return fmt.Errorf("%s: %s", strings.Join(addr, "."), err) - } - - // Make space for additional elements in the address - addrCopy := make([]string, len(addr), len(addr)+1) - copy(addrCopy, addr) - - // Set each element in turn - var err error - for k1, v1 := range v { - if err = w.set(append(addrCopy, k1), v1); err != nil { - break - } - } - if err != nil { - for k1, _ := range v { - w.set(append(addrCopy, k1), nil) - } - } - - return err -} - -func (w *MapFieldWriter) setPrimitive( - addr []string, - v interface{}, - schema *Schema) error { - k := strings.Join(addr, ".") - - if v == nil { - // The empty string here means the value is removed. - w.result[k] = "" - return nil - } - - var set string - switch schema.Type { - case TypeBool: - var b bool - if err := mapstructure.Decode(v, &b); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - set = strconv.FormatBool(b) - case TypeString: - if err := mapstructure.Decode(v, &set); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - case TypeInt: - var n int - if err := mapstructure.Decode(v, &n); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - set = strconv.FormatInt(int64(n), 10) - case TypeFloat: - var n float64 - if err := mapstructure.Decode(v, &n); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - set = strconv.FormatFloat(float64(n), 'G', -1, 64) - default: - return fmt.Errorf("Unknown type: %#v", schema.Type) - } - - w.result[k] = set - return nil -} - -func (w *MapFieldWriter) setSet( - addr []string, - value interface{}, - schema *Schema) error { - addrCopy := make([]string, len(addr), len(addr)+1) - copy(addrCopy, addr) - k := strings.Join(addr, ".") - - if value == nil { - w.result[k+".#"] = "0" - return nil - } - - // If it is a slice, then we have to turn it into a *Set so that - // we get the proper order back based on the hash code. - if v := reflect.ValueOf(value); v.Kind() == reflect.Slice { - // Build a temp *ResourceData to use for the conversion - tempAddr := addr[len(addr)-1:] - tempSchema := *schema - tempSchema.Type = TypeList - tempSchemaMap := map[string]*Schema{tempAddr[0]: &tempSchema} - tempW := &MapFieldWriter{Schema: tempSchemaMap} - - // Set the entire list, this lets us get values out of it - if err := tempW.WriteField(tempAddr, value); err != nil { - return err - } - - // Build the set by going over the list items in order and - // hashing them into the set. The reason we go over the list and - // not the `value` directly is because this forces all types - // to become []interface{} (generic) instead of []string, which - // most hash functions are expecting. - s := schema.ZeroValue().(*Set) - tempR := &MapFieldReader{ - Map: BasicMapReader(tempW.Map()), - Schema: tempSchemaMap, - } - for i := 0; i < v.Len(); i++ { - is := strconv.FormatInt(int64(i), 10) - result, err := tempR.ReadField(append(tempAddr, is)) - if err != nil { - return err - } - if !result.Exists { - panic("set item just set doesn't exist") - } - - s.Add(result.Value) - } - - value = s - } - - // Clear any keys that match the set address first. This is necessary because - // it's always possible and sometimes may be necessary to write to a certain - // writer layer more than once with different set data each time, which will - // lead to different keys being inserted, which can lead to determinism - // problems when the old data isn't wiped first. - w.clearTree(addr) - - if value.(*Set) == nil { - w.result[k+".#"] = "0" - return nil - } - - for code, elem := range value.(*Set).m { - if err := w.set(append(addrCopy, code), elem); err != nil { - return err - } - } - - w.result[k+".#"] = strconv.Itoa(value.(*Set).Len()) - return nil -} diff --git a/helper/schema/field_writer_map_test.go b/helper/schema/field_writer_map_test.go deleted file mode 100644 index d1a7932aa..000000000 --- a/helper/schema/field_writer_map_test.go +++ /dev/null @@ -1,547 +0,0 @@ -package schema - -import ( - "reflect" - "testing" -) - -func TestMapFieldWriter_impl(t *testing.T) { - var _ FieldWriter = new(MapFieldWriter) -} - -func TestMapFieldWriter(t *testing.T) { - schema := map[string]*Schema{ - "bool": &Schema{Type: TypeBool}, - "int": &Schema{Type: TypeInt}, - "string": &Schema{Type: TypeString}, - "list": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeString}, - }, - "listInt": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - "listResource": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "value": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - }, - }, - "map": &Schema{Type: TypeMap}, - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - "setDeep": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, - } - - cases := map[string]struct { - Addr []string - Value interface{} - Err bool - Out map[string]string - }{ - "noexist": { - []string{"noexist"}, - 42, - true, - map[string]string{}, - }, - - "bool": { - []string{"bool"}, - false, - false, - map[string]string{ - "bool": "false", - }, - }, - - "int": { - []string{"int"}, - 42, - false, - map[string]string{ - "int": "42", - }, - }, - - "string": { - []string{"string"}, - "42", - false, - map[string]string{ - "string": "42", - }, - }, - - "string nil": { - []string{"string"}, - nil, - false, - map[string]string{ - "string": "", - }, - }, - - "list of resources": { - []string{"listResource"}, - []interface{}{ - map[string]interface{}{ - "value": 80, - }, - }, - false, - map[string]string{ - "listResource.#": "1", - "listResource.0.value": "80", - }, - }, - - "list of resources empty": { - []string{"listResource"}, - []interface{}{}, - false, - map[string]string{ - "listResource.#": "0", - }, - }, - - "list of resources nil": { - []string{"listResource"}, - nil, - false, - map[string]string{ - "listResource.#": "0", - }, - }, - - "list of strings": { - []string{"list"}, - []interface{}{"foo", "bar"}, - false, - map[string]string{ - "list.#": "2", - "list.0": "foo", - "list.1": "bar", - }, - }, - - "list element": { - []string{"list", "0"}, - "string", - true, - map[string]string{}, - }, - - "map": { - []string{"map"}, - map[string]interface{}{"foo": "bar"}, - false, - map[string]string{ - "map.%": "1", - "map.foo": "bar", - }, - }, - - "map delete": { - []string{"map"}, - nil, - false, - map[string]string{ - "map": "", - }, - }, - - "map element": { - []string{"map", "foo"}, - "bar", - true, - map[string]string{}, - }, - - "set": { - []string{"set"}, - []interface{}{1, 2, 5}, - false, - map[string]string{ - "set.#": "3", - "set.1": "1", - "set.2": "2", - "set.5": "5", - }, - }, - - "set nil": { - []string{"set"}, - nil, - false, - map[string]string{ - "set.#": "0", - }, - }, - - "set typed nil": { - []string{"set"}, - func() *Set { return nil }(), - false, - map[string]string{ - "set.#": "0", - }, - }, - - "set resource": { - []string{"setDeep"}, - []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "foo", - }, - map[string]interface{}{ - "index": 50, - "value": "bar", - }, - }, - false, - map[string]string{ - "setDeep.#": "2", - "setDeep.10.index": "10", - "setDeep.10.value": "foo", - "setDeep.50.index": "50", - "setDeep.50.value": "bar", - }, - }, - - "set element": { - []string{"set", "5"}, - 5, - true, - map[string]string{}, - }, - - "full object": { - nil, - map[string]interface{}{ - "string": "foo", - "list": []interface{}{"foo", "bar"}, - }, - false, - map[string]string{ - "string": "foo", - "list.#": "2", - "list.0": "foo", - "list.1": "bar", - }, - }, - } - - for name, tc := range cases { - w := &MapFieldWriter{Schema: schema} - err := w.WriteField(tc.Addr, tc.Value) - if err != nil != tc.Err { - t.Fatalf("%s: err: %s", name, err) - } - - actual := w.Map() - if !reflect.DeepEqual(actual, tc.Out) { - t.Fatalf("%s: bad: %#v", name, actual) - } - } -} - -func TestMapFieldWriterCleanSet(t *testing.T) { - schema := map[string]*Schema{ - "setDeep": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "value": &Schema{Type: TypeString}, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["index"].(int) - }, - }, - } - - values := []struct { - Addr []string - Value interface{} - Out map[string]string - }{ - { - []string{"setDeep"}, - []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "foo", - }, - map[string]interface{}{ - "index": 50, - "value": "bar", - }, - }, - map[string]string{ - "setDeep.#": "2", - "setDeep.10.index": "10", - "setDeep.10.value": "foo", - "setDeep.50.index": "50", - "setDeep.50.value": "bar", - }, - }, - { - []string{"setDeep"}, - []interface{}{ - map[string]interface{}{ - "index": 20, - "value": "baz", - }, - map[string]interface{}{ - "index": 60, - "value": "qux", - }, - }, - map[string]string{ - "setDeep.#": "2", - "setDeep.20.index": "20", - "setDeep.20.value": "baz", - "setDeep.60.index": "60", - "setDeep.60.value": "qux", - }, - }, - { - []string{"setDeep"}, - []interface{}{ - map[string]interface{}{ - "index": 30, - "value": "one", - }, - map[string]interface{}{ - "index": 70, - "value": "two", - }, - }, - map[string]string{ - "setDeep.#": "2", - "setDeep.30.index": "30", - "setDeep.30.value": "one", - "setDeep.70.index": "70", - "setDeep.70.value": "two", - }, - }, - } - - w := &MapFieldWriter{Schema: schema} - - for n, tc := range values { - err := w.WriteField(tc.Addr, tc.Value) - if err != nil { - t.Fatalf("%d: err: %s", n, err) - } - - actual := w.Map() - if !reflect.DeepEqual(actual, tc.Out) { - t.Fatalf("%d: bad: %#v", n, actual) - } - } -} - -func TestMapFieldWriterCleanList(t *testing.T) { - schema := map[string]*Schema{ - "listDeep": &Schema{ - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "thing1": &Schema{Type: TypeString}, - "thing2": &Schema{Type: TypeString}, - }, - }, - }, - } - - values := []struct { - Addr []string - Value interface{} - Out map[string]string - }{ - { - // Base list - []string{"listDeep"}, - []interface{}{ - map[string]interface{}{ - "thing1": "a", - "thing2": "b", - }, - map[string]interface{}{ - "thing1": "c", - "thing2": "d", - }, - map[string]interface{}{ - "thing1": "e", - "thing2": "f", - }, - map[string]interface{}{ - "thing1": "g", - "thing2": "h", - }, - }, - map[string]string{ - "listDeep.#": "4", - "listDeep.0.thing1": "a", - "listDeep.0.thing2": "b", - "listDeep.1.thing1": "c", - "listDeep.1.thing2": "d", - "listDeep.2.thing1": "e", - "listDeep.2.thing2": "f", - "listDeep.3.thing1": "g", - "listDeep.3.thing2": "h", - }, - }, - { - // Remove an element - []string{"listDeep"}, - []interface{}{ - map[string]interface{}{ - "thing1": "a", - "thing2": "b", - }, - map[string]interface{}{ - "thing1": "c", - "thing2": "d", - }, - map[string]interface{}{ - "thing1": "e", - "thing2": "f", - }, - }, - map[string]string{ - "listDeep.#": "3", - "listDeep.0.thing1": "a", - "listDeep.0.thing2": "b", - "listDeep.1.thing1": "c", - "listDeep.1.thing2": "d", - "listDeep.2.thing1": "e", - "listDeep.2.thing2": "f", - }, - }, - { - // Rewrite with missing keys. This should normally not be necessary, as - // hopefully the writers are writing zero values as necessary, but for - // brevity we want to make sure that what exists in the writer is exactly - // what the last write looked like coming from the provider. - []string{"listDeep"}, - []interface{}{ - map[string]interface{}{ - "thing1": "a", - }, - map[string]interface{}{ - "thing1": "c", - }, - map[string]interface{}{ - "thing1": "e", - }, - }, - map[string]string{ - "listDeep.#": "3", - "listDeep.0.thing1": "a", - "listDeep.1.thing1": "c", - "listDeep.2.thing1": "e", - }, - }, - } - - w := &MapFieldWriter{Schema: schema} - - for n, tc := range values { - err := w.WriteField(tc.Addr, tc.Value) - if err != nil { - t.Fatalf("%d: err: %s", n, err) - } - - actual := w.Map() - if !reflect.DeepEqual(actual, tc.Out) { - t.Fatalf("%d: bad: %#v", n, actual) - } - } -} - -func TestMapFieldWriterCleanMap(t *testing.T) { - schema := map[string]*Schema{ - "map": &Schema{ - Type: TypeMap, - }, - } - - values := []struct { - Value interface{} - Out map[string]string - }{ - { - // Base map - map[string]interface{}{ - "thing1": "a", - "thing2": "b", - "thing3": "c", - "thing4": "d", - }, - map[string]string{ - "map.%": "4", - "map.thing1": "a", - "map.thing2": "b", - "map.thing3": "c", - "map.thing4": "d", - }, - }, - { - // Base map - map[string]interface{}{ - "thing1": "a", - "thing2": "b", - "thing4": "d", - }, - map[string]string{ - "map.%": "3", - "map.thing1": "a", - "map.thing2": "b", - "map.thing4": "d", - }, - }, - } - - w := &MapFieldWriter{Schema: schema} - - for n, tc := range values { - err := w.WriteField([]string{"map"}, tc.Value) - if err != nil { - t.Fatalf("%d: err: %s", n, err) - } - - actual := w.Map() - if !reflect.DeepEqual(actual, tc.Out) { - t.Fatalf("%d: bad: %#v", n, actual) - } - } -} diff --git a/helper/schema/getsource_string.go b/helper/schema/getsource_string.go deleted file mode 100644 index 0184d7b08..000000000 --- a/helper/schema/getsource_string.go +++ /dev/null @@ -1,46 +0,0 @@ -// Code generated by "stringer -type=getSource resource_data_get_source.go"; DO NOT EDIT. - -package schema - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[getSourceState-1] - _ = x[getSourceConfig-2] - _ = x[getSourceDiff-4] - _ = x[getSourceSet-8] - _ = x[getSourceExact-16] - _ = x[getSourceLevelMask-15] -} - -const ( - _getSource_name_0 = "getSourceStategetSourceConfig" - _getSource_name_1 = "getSourceDiff" - _getSource_name_2 = "getSourceSet" - _getSource_name_3 = "getSourceLevelMaskgetSourceExact" -) - -var ( - _getSource_index_0 = [...]uint8{0, 14, 29} - _getSource_index_3 = [...]uint8{0, 18, 32} -) - -func (i getSource) String() string { - switch { - case 1 <= i && i <= 2: - i -= 1 - return _getSource_name_0[_getSource_index_0[i]:_getSource_index_0[i+1]] - case i == 4: - return _getSource_name_1 - case i == 8: - return _getSource_name_2 - case 15 <= i && i <= 16: - i -= 15 - return _getSource_name_3[_getSource_index_3[i]:_getSource_index_3[i+1]] - default: - return "getSource(" + strconv.FormatInt(int64(i), 10) + ")" - } -} diff --git a/helper/schema/provider.go b/helper/schema/provider.go deleted file mode 100644 index 59dc750ee..000000000 --- a/helper/schema/provider.go +++ /dev/null @@ -1,477 +0,0 @@ -package schema - -import ( - "context" - "errors" - "fmt" - "sort" - "sync" - - multierror "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/terraform" -) - -var ReservedProviderFields = []string{ - "alias", - "version", -} - -// Provider represents a resource provider in Terraform, and properly -// implements all of the ResourceProvider API. -// -// By defining a schema for the configuration of the provider, the -// map of supporting resources, and a configuration function, the schema -// framework takes over and handles all the provider operations for you. -// -// After defining the provider structure, it is unlikely that you'll require any -// of the methods on Provider itself. -type Provider struct { - // Schema is the schema for the configuration of this provider. If this - // provider has no configuration, this can be omitted. - // - // The keys of this map are the configuration keys, and the value is - // the schema describing the value of the configuration. - Schema map[string]*Schema - - // ResourcesMap is the list of available resources that this provider - // can manage, along with their Resource structure defining their - // own schemas and CRUD operations. - // - // Provider automatically handles routing operations such as Apply, - // Diff, etc. to the proper resource. - ResourcesMap map[string]*Resource - - // DataSourcesMap is the collection of available data sources that - // this provider implements, with a Resource instance defining - // the schema and Read operation of each. - // - // Resource instances for data sources must have a Read function - // and must *not* implement Create, Update or Delete. - DataSourcesMap map[string]*Resource - - // ProviderMetaSchema is the schema for the configuration of the meta - // information for this provider. If this provider has no meta info, - // this can be omitted. This functionality is currently experimental - // and subject to change or break without warning; it should only be - // used by providers that are collaborating on its use with the - // Terraform team. - ProviderMetaSchema map[string]*Schema - - // ConfigureFunc is a function for configuring the provider. If the - // provider doesn't need to be configured, this can be omitted. - // - // See the ConfigureFunc documentation for more information. - ConfigureFunc ConfigureFunc - - // MetaReset is called by TestReset to reset any state stored in the meta - // interface. This is especially important if the StopContext is stored by - // the provider. - MetaReset func() error - - meta interface{} - - // a mutex is required because TestReset can directly replace the stopCtx - stopMu sync.Mutex - stopCtx context.Context - stopCtxCancel context.CancelFunc - stopOnce sync.Once - - TerraformVersion string -} - -// ConfigureFunc is the function used to configure a Provider. -// -// The interface{} value returned by this function is stored and passed into -// the subsequent resources as the meta parameter. This return value is -// usually used to pass along a configured API client, a configuration -// structure, etc. -type ConfigureFunc func(*ResourceData) (interface{}, error) - -// InternalValidate should be called to validate the structure -// of the provider. -// -// This should be called in a unit test for any provider to verify -// before release that a provider is properly configured for use with -// this library. -func (p *Provider) InternalValidate() error { - if p == nil { - return errors.New("provider is nil") - } - - var validationErrors error - sm := schemaMap(p.Schema) - if err := sm.InternalValidate(sm); err != nil { - validationErrors = multierror.Append(validationErrors, err) - } - - // Provider-specific checks - for k, _ := range sm { - if isReservedProviderFieldName(k) { - return fmt.Errorf("%s is a reserved field name for a provider", k) - } - } - - for k, r := range p.ResourcesMap { - if err := r.InternalValidate(nil, true); err != nil { - validationErrors = multierror.Append(validationErrors, fmt.Errorf("resource %s: %s", k, err)) - } - } - - for k, r := range p.DataSourcesMap { - if err := r.InternalValidate(nil, false); err != nil { - validationErrors = multierror.Append(validationErrors, fmt.Errorf("data source %s: %s", k, err)) - } - } - - return validationErrors -} - -func isReservedProviderFieldName(name string) bool { - for _, reservedName := range ReservedProviderFields { - if name == reservedName { - return true - } - } - return false -} - -// Meta returns the metadata associated with this provider that was -// returned by the Configure call. It will be nil until Configure is called. -func (p *Provider) Meta() interface{} { - return p.meta -} - -// SetMeta can be used to forcefully set the Meta object of the provider. -// Note that if Configure is called the return value will override anything -// set here. -func (p *Provider) SetMeta(v interface{}) { - p.meta = v -} - -// Stopped reports whether the provider has been stopped or not. -func (p *Provider) Stopped() bool { - ctx := p.StopContext() - select { - case <-ctx.Done(): - return true - default: - return false - } -} - -// StopCh returns a channel that is closed once the provider is stopped. -func (p *Provider) StopContext() context.Context { - p.stopOnce.Do(p.stopInit) - - p.stopMu.Lock() - defer p.stopMu.Unlock() - - return p.stopCtx -} - -func (p *Provider) stopInit() { - p.stopMu.Lock() - defer p.stopMu.Unlock() - - p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background()) -} - -// Stop implementation of terraform.ResourceProvider interface. -func (p *Provider) Stop() error { - p.stopOnce.Do(p.stopInit) - - p.stopMu.Lock() - defer p.stopMu.Unlock() - - p.stopCtxCancel() - return nil -} - -// TestReset resets any state stored in the Provider, and will call TestReset -// on Meta if it implements the TestProvider interface. -// This may be used to reset the schema.Provider at the start of a test, and is -// automatically called by resource.Test. -func (p *Provider) TestReset() error { - p.stopInit() - if p.MetaReset != nil { - return p.MetaReset() - } - return nil -} - -// GetSchema implementation of terraform.ResourceProvider interface -func (p *Provider) GetSchema(req *terraform.ProviderSchemaRequest) (*terraform.ProviderSchema, error) { - resourceTypes := map[string]*configschema.Block{} - dataSources := map[string]*configschema.Block{} - - for _, name := range req.ResourceTypes { - if r, exists := p.ResourcesMap[name]; exists { - resourceTypes[name] = r.CoreConfigSchema() - } - } - for _, name := range req.DataSources { - if r, exists := p.DataSourcesMap[name]; exists { - dataSources[name] = r.CoreConfigSchema() - } - } - - return &terraform.ProviderSchema{ - Provider: schemaMap(p.Schema).CoreConfigSchema(), - ResourceTypes: resourceTypes, - DataSources: dataSources, - }, nil -} - -// Input implementation of terraform.ResourceProvider interface. -func (p *Provider) Input( - input terraform.UIInput, - c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - return schemaMap(p.Schema).Input(input, c) -} - -// Validate implementation of terraform.ResourceProvider interface. -func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) { - if err := p.InternalValidate(); err != nil { - return nil, []error{fmt.Errorf( - "Internal validation of the provider failed! This is always a bug\n"+ - "with the provider itself, and not a user issue. Please report\n"+ - "this bug:\n\n%s", err)} - } - - return schemaMap(p.Schema).Validate(c) -} - -// ValidateResource implementation of terraform.ResourceProvider interface. -func (p *Provider) ValidateResource( - t string, c *terraform.ResourceConfig) ([]string, []error) { - r, ok := p.ResourcesMap[t] - if !ok { - return nil, []error{fmt.Errorf( - "Provider doesn't support resource: %s", t)} - } - - return r.Validate(c) -} - -// Configure implementation of terraform.ResourceProvider interface. -func (p *Provider) Configure(c *terraform.ResourceConfig) error { - // No configuration - if p.ConfigureFunc == nil { - return nil - } - - sm := schemaMap(p.Schema) - - // Get a ResourceData for this configuration. To do this, we actually - // generate an intermediary "diff" although that is never exposed. - diff, err := sm.Diff(nil, c, nil, p.meta, true) - if err != nil { - return err - } - - data, err := sm.Data(nil, diff) - if err != nil { - return err - } - - meta, err := p.ConfigureFunc(data) - if err != nil { - return err - } - - p.meta = meta - return nil -} - -// Apply implementation of terraform.ResourceProvider interface. -func (p *Provider) Apply( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - d *terraform.InstanceDiff) (*terraform.InstanceState, error) { - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - return r.Apply(s, d, p.meta) -} - -// Diff implementation of terraform.ResourceProvider interface. -func (p *Provider) Diff( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - return r.Diff(s, c, p.meta) -} - -// SimpleDiff is used by the new protocol wrappers to get a diff that doesn't -// attempt to calculate ignore_changes. -func (p *Provider) SimpleDiff( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - return r.simpleDiff(s, c, p.meta) -} - -// Refresh implementation of terraform.ResourceProvider interface. -func (p *Provider) Refresh( - info *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - return r.Refresh(s, p.meta) -} - -// Resources implementation of terraform.ResourceProvider interface. -func (p *Provider) Resources() []terraform.ResourceType { - keys := make([]string, 0, len(p.ResourcesMap)) - for k := range p.ResourcesMap { - keys = append(keys, k) - } - sort.Strings(keys) - - result := make([]terraform.ResourceType, 0, len(keys)) - for _, k := range keys { - resource := p.ResourcesMap[k] - - // This isn't really possible (it'd fail InternalValidate), but - // we do it anyways to avoid a panic. - if resource == nil { - resource = &Resource{} - } - - result = append(result, terraform.ResourceType{ - Name: k, - Importable: resource.Importer != nil, - - // Indicates that a provider is compiled against a new enough - // version of core to support the GetSchema method. - SchemaAvailable: true, - }) - } - - return result -} - -func (p *Provider) ImportState( - info *terraform.InstanceInfo, - id string) ([]*terraform.InstanceState, error) { - // Find the resource - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - // If it doesn't support import, error - if r.Importer == nil { - return nil, fmt.Errorf("resource %s doesn't support import", info.Type) - } - - // Create the data - data := r.Data(nil) - data.SetId(id) - data.SetType(info.Type) - - // Call the import function - results := []*ResourceData{data} - if r.Importer.State != nil { - var err error - results, err = r.Importer.State(data, p.meta) - if err != nil { - return nil, err - } - } - - // Convert the results to InstanceState values and return it - states := make([]*terraform.InstanceState, len(results)) - for i, r := range results { - states[i] = r.State() - } - - // Verify that all are non-nil. If there are any nil the error - // isn't obvious so we circumvent that with a friendlier error. - for _, s := range states { - if s == nil { - return nil, fmt.Errorf( - "nil entry in ImportState results. This is always a bug with\n" + - "the resource that is being imported. Please report this as\n" + - "a bug to Terraform.") - } - } - - return states, nil -} - -// ValidateDataSource implementation of terraform.ResourceProvider interface. -func (p *Provider) ValidateDataSource( - t string, c *terraform.ResourceConfig) ([]string, []error) { - r, ok := p.DataSourcesMap[t] - if !ok { - return nil, []error{fmt.Errorf( - "Provider doesn't support data source: %s", t)} - } - - return r.Validate(c) -} - -// ReadDataDiff implementation of terraform.ResourceProvider interface. -func (p *Provider) ReadDataDiff( - info *terraform.InstanceInfo, - c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { - - r, ok := p.DataSourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown data source: %s", info.Type) - } - - return r.Diff(nil, c, p.meta) -} - -// RefreshData implementation of terraform.ResourceProvider interface. -func (p *Provider) ReadDataApply( - info *terraform.InstanceInfo, - d *terraform.InstanceDiff) (*terraform.InstanceState, error) { - - r, ok := p.DataSourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown data source: %s", info.Type) - } - - return r.ReadDataApply(d, p.meta) -} - -// DataSources implementation of terraform.ResourceProvider interface. -func (p *Provider) DataSources() []terraform.DataSource { - keys := make([]string, 0, len(p.DataSourcesMap)) - for k, _ := range p.DataSourcesMap { - keys = append(keys, k) - } - sort.Strings(keys) - - result := make([]terraform.DataSource, 0, len(keys)) - for _, k := range keys { - result = append(result, terraform.DataSource{ - Name: k, - - // Indicates that a provider is compiled against a new enough - // version of core to support the GetSchema method. - SchemaAvailable: true, - }) - } - - return result -} diff --git a/helper/schema/provider_test.go b/helper/schema/provider_test.go deleted file mode 100644 index 1f9b5e8bf..000000000 --- a/helper/schema/provider_test.go +++ /dev/null @@ -1,620 +0,0 @@ -package schema - -import ( - "fmt" - "reflect" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/terraform" -) - -func TestProvider_impl(t *testing.T) { - var _ terraform.ResourceProvider = new(Provider) -} - -func TestProviderGetSchema(t *testing.T) { - // This functionality is already broadly tested in core_schema_test.go, - // so this is just to ensure that the call passes through correctly. - p := &Provider{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeString, - Required: true, - }, - }, - ResourcesMap: map[string]*Resource{ - "foo": &Resource{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeString, - Required: true, - }, - }, - }, - }, - DataSourcesMap: map[string]*Resource{ - "baz": &Resource{ - Schema: map[string]*Schema{ - "bur": { - Type: TypeString, - Required: true, - }, - }, - }, - }, - } - - want := &terraform.ProviderSchema{ - Provider: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": &configschema.Attribute{ - Type: cty.String, - Required: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }, - ResourceTypes: map[string]*configschema.Block{ - "foo": testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": &configschema.Attribute{ - Type: cty.String, - Required: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - DataSources: map[string]*configschema.Block{ - "baz": testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bur": &configschema.Attribute{ - Type: cty.String, - Required: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - } - got, err := p.GetSchema(&terraform.ProviderSchemaRequest{ - ResourceTypes: []string{"foo", "bar"}, - DataSources: []string{"baz", "bar"}, - }) - if err != nil { - t.Fatalf("unexpected error %s", err) - } - - if !cmp.Equal(got, want, equateEmpty, typeComparer) { - t.Error("wrong result:\n", cmp.Diff(got, want, equateEmpty, typeComparer)) - } -} - -func TestProviderConfigure(t *testing.T) { - cases := []struct { - P *Provider - Config map[string]interface{} - Err bool - }{ - { - P: &Provider{}, - Config: nil, - Err: false, - }, - - { - P: &Provider{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ConfigureFunc: func(d *ResourceData) (interface{}, error) { - if d.Get("foo").(int) == 42 { - return nil, nil - } - - return nil, fmt.Errorf("nope") - }, - }, - Config: map[string]interface{}{ - "foo": 42, - }, - Err: false, - }, - - { - P: &Provider{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ConfigureFunc: func(d *ResourceData) (interface{}, error) { - if d.Get("foo").(int) == 42 { - return nil, nil - } - - return nil, fmt.Errorf("nope") - }, - }, - Config: map[string]interface{}{ - "foo": 52, - }, - Err: true, - }, - } - - for i, tc := range cases { - c := terraform.NewResourceConfigRaw(tc.Config) - err := tc.P.Configure(c) - if err != nil != tc.Err { - t.Fatalf("%d: %s", i, err) - } - } -} - -func TestProviderResources(t *testing.T) { - cases := []struct { - P *Provider - Result []terraform.ResourceType - }{ - { - P: &Provider{}, - Result: []terraform.ResourceType{}, - }, - - { - P: &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": nil, - "bar": nil, - }, - }, - Result: []terraform.ResourceType{ - terraform.ResourceType{Name: "bar", SchemaAvailable: true}, - terraform.ResourceType{Name: "foo", SchemaAvailable: true}, - }, - }, - - { - P: &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": nil, - "bar": &Resource{Importer: &ResourceImporter{}}, - "baz": nil, - }, - }, - Result: []terraform.ResourceType{ - terraform.ResourceType{Name: "bar", Importable: true, SchemaAvailable: true}, - terraform.ResourceType{Name: "baz", SchemaAvailable: true}, - terraform.ResourceType{Name: "foo", SchemaAvailable: true}, - }, - }, - } - - for i, tc := range cases { - actual := tc.P.Resources() - if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("%d: %#v", i, actual) - } - } -} - -func TestProviderDataSources(t *testing.T) { - cases := []struct { - P *Provider - Result []terraform.DataSource - }{ - { - P: &Provider{}, - Result: []terraform.DataSource{}, - }, - - { - P: &Provider{ - DataSourcesMap: map[string]*Resource{ - "foo": nil, - "bar": nil, - }, - }, - Result: []terraform.DataSource{ - terraform.DataSource{Name: "bar", SchemaAvailable: true}, - terraform.DataSource{Name: "foo", SchemaAvailable: true}, - }, - }, - } - - for i, tc := range cases { - actual := tc.P.DataSources() - if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("%d: got %#v; want %#v", i, actual, tc.Result) - } - } -} - -func TestProviderValidate(t *testing.T) { - cases := []struct { - P *Provider - Config map[string]interface{} - Err bool - }{ - { - P: &Provider{ - Schema: map[string]*Schema{ - "foo": &Schema{}, - }, - }, - Config: nil, - Err: true, - }, - } - - for i, tc := range cases { - c := terraform.NewResourceConfigRaw(tc.Config) - _, es := tc.P.Validate(c) - if len(es) > 0 != tc.Err { - t.Fatalf("%d: %#v", i, es) - } - } -} - -func TestProviderDiff_legacyTimeoutType(t *testing.T) { - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "blah": &Resource{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(10 * time.Minute), - }, - }, - }, - } - - invalidCfg := map[string]interface{}{ - "foo": 42, - "timeouts": []interface{}{ - map[string]interface{}{ - "create": "40m", - }, - }, - } - ic := terraform.NewResourceConfigRaw(invalidCfg) - _, err := p.Diff( - &terraform.InstanceInfo{ - Type: "blah", - }, - nil, - ic, - ) - if err != nil { - t.Fatal(err) - } -} - -func TestProviderDiff_timeoutInvalidValue(t *testing.T) { - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "blah": &Resource{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(10 * time.Minute), - }, - }, - }, - } - - invalidCfg := map[string]interface{}{ - "foo": 42, - "timeouts": map[string]interface{}{ - "create": "invalid", - }, - } - ic := terraform.NewResourceConfigRaw(invalidCfg) - _, err := p.Diff( - &terraform.InstanceInfo{ - Type: "blah", - }, - nil, - ic, - ) - if err == nil { - t.Fatal("Expected provider.Diff to fail with invalid timeout value") - } - expectedErrMsg := `time: invalid duration "invalid"` - if !strings.Contains(err.Error(), expectedErrMsg) { - t.Fatalf("Unexpected error message: %q\nExpected message to contain %q", - err.Error(), - expectedErrMsg) - } -} - -func TestProviderValidateResource(t *testing.T) { - cases := []struct { - P *Provider - Type string - Config map[string]interface{} - Err bool - }{ - { - P: &Provider{}, - Type: "foo", - Config: nil, - Err: true, - }, - - { - P: &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": &Resource{}, - }, - }, - Type: "foo", - Config: nil, - Err: false, - }, - } - - for i, tc := range cases { - c := terraform.NewResourceConfigRaw(tc.Config) - _, es := tc.P.ValidateResource(tc.Type, c) - if len(es) > 0 != tc.Err { - t.Fatalf("%d: %#v", i, es) - } - } -} - -func TestProviderImportState_default(t *testing.T) { - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": &Resource{ - Importer: &ResourceImporter{}, - }, - }, - } - - states, err := p.ImportState(&terraform.InstanceInfo{ - Type: "foo", - }, "bar") - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(states) != 1 { - t.Fatalf("bad: %#v", states) - } - if states[0].ID != "bar" { - t.Fatalf("bad: %#v", states) - } -} - -func TestProviderImportState_setsId(t *testing.T) { - var val string - stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) { - val = d.Id() - return []*ResourceData{d}, nil - } - - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": &Resource{ - Importer: &ResourceImporter{ - State: stateFunc, - }, - }, - }, - } - - _, err := p.ImportState(&terraform.InstanceInfo{ - Type: "foo", - }, "bar") - if err != nil { - t.Fatalf("err: %s", err) - } - - if val != "bar" { - t.Fatal("should set id") - } -} - -func TestProviderImportState_setsType(t *testing.T) { - var tVal string - stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) { - d.SetId("foo") - tVal = d.State().Ephemeral.Type - return []*ResourceData{d}, nil - } - - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": &Resource{ - Importer: &ResourceImporter{ - State: stateFunc, - }, - }, - }, - } - - _, err := p.ImportState(&terraform.InstanceInfo{ - Type: "foo", - }, "bar") - if err != nil { - t.Fatalf("err: %s", err) - } - - if tVal != "foo" { - t.Fatal("should set type") - } -} - -func TestProviderMeta(t *testing.T) { - p := new(Provider) - if v := p.Meta(); v != nil { - t.Fatalf("bad: %#v", v) - } - - expected := 42 - p.SetMeta(42) - if v := p.Meta(); !reflect.DeepEqual(v, expected) { - t.Fatalf("bad: %#v", v) - } -} - -func TestProviderStop(t *testing.T) { - var p Provider - - if p.Stopped() { - t.Fatal("should not be stopped") - } - - // Verify stopch blocks - ch := p.StopContext().Done() - select { - case <-ch: - t.Fatal("should not be stopped") - case <-time.After(10 * time.Millisecond): - } - - // Stop it - if err := p.Stop(); err != nil { - t.Fatalf("err: %s", err) - } - - // Verify - if !p.Stopped() { - t.Fatal("should be stopped") - } - - select { - case <-ch: - case <-time.After(10 * time.Millisecond): - t.Fatal("should be stopped") - } -} - -func TestProviderStop_stopFirst(t *testing.T) { - var p Provider - - // Stop it - if err := p.Stop(); err != nil { - t.Fatalf("err: %s", err) - } - - // Verify - if !p.Stopped() { - t.Fatal("should be stopped") - } - - select { - case <-p.StopContext().Done(): - case <-time.After(10 * time.Millisecond): - t.Fatal("should be stopped") - } -} - -func TestProviderReset(t *testing.T) { - var p Provider - stopCtx := p.StopContext() - p.MetaReset = func() error { - stopCtx = p.StopContext() - return nil - } - - // cancel the current context - p.Stop() - - if err := p.TestReset(); err != nil { - t.Fatal(err) - } - - // the first context should have been replaced - if err := stopCtx.Err(); err != nil { - t.Fatal(err) - } - - // we should not get a canceled context here either - if err := p.StopContext().Err(); err != nil { - t.Fatal(err) - } -} - -func TestProvider_InternalValidate(t *testing.T) { - cases := []struct { - P *Provider - ExpectedErr error - }{ - { - P: &Provider{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeBool, - Optional: true, - }, - }, - }, - ExpectedErr: nil, - }, - { // Reserved resource fields should be allowed in provider block - P: &Provider{ - Schema: map[string]*Schema{ - "provisioner": { - Type: TypeString, - Optional: true, - }, - "count": { - Type: TypeInt, - Optional: true, - }, - }, - }, - ExpectedErr: nil, - }, - { // Reserved provider fields should not be allowed - P: &Provider{ - Schema: map[string]*Schema{ - "alias": { - Type: TypeString, - Optional: true, - }, - }, - }, - ExpectedErr: fmt.Errorf("%s is a reserved field name for a provider", "alias"), - }, - } - - for i, tc := range cases { - err := tc.P.InternalValidate() - if tc.ExpectedErr == nil { - if err != nil { - t.Fatalf("%d: Error returned (expected no error): %s", i, err) - } - continue - } - if tc.ExpectedErr != nil && err == nil { - t.Fatalf("%d: Expected error (%s), but no error returned", i, tc.ExpectedErr) - } - if err.Error() != tc.ExpectedErr.Error() { - t.Fatalf("%d: Errors don't match. Expected: %#v Given: %#v", i, tc.ExpectedErr, err) - } - } -} diff --git a/helper/schema/provisioner.go b/helper/schema/provisioner.go deleted file mode 100644 index eee155bfb..000000000 --- a/helper/schema/provisioner.go +++ /dev/null @@ -1,205 +0,0 @@ -package schema - -import ( - "context" - "errors" - "fmt" - "sync" - - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/terraform" -) - -// Provisioner represents a resource provisioner in Terraform and properly -// implements all of the ResourceProvisioner API. -// -// This higher level structure makes it much easier to implement a new or -// custom provisioner for Terraform. -// -// The function callbacks for this structure are all passed a context object. -// This context object has a number of pre-defined values that can be accessed -// via the global functions defined in context.go. -type Provisioner struct { - // ConnSchema is the schema for the connection settings for this - // provisioner. - // - // The keys of this map are the configuration keys, and the value is - // the schema describing the value of the configuration. - // - // NOTE: The value of connection keys can only be strings for now. - ConnSchema map[string]*Schema - - // Schema is the schema for the usage of this provisioner. - // - // The keys of this map are the configuration keys, and the value is - // the schema describing the value of the configuration. - Schema map[string]*Schema - - // ApplyFunc is the function for executing the provisioner. This is required. - // It is given a context. See the Provisioner struct docs for more - // information. - ApplyFunc func(ctx context.Context) error - - // ValidateFunc is a function for extended validation. This is optional - // and should be used when individual field validation is not enough. - ValidateFunc func(*terraform.ResourceConfig) ([]string, []error) - - stopCtx context.Context - stopCtxCancel context.CancelFunc - stopOnce sync.Once -} - -// Keys that can be used to access data in the context parameters for -// Provisioners. -var ( - connDataInvalid = contextKey("data invalid") - - // This returns a *ResourceData for the connection information. - // Guaranteed to never be nil. - ProvConnDataKey = contextKey("provider conn data") - - // This returns a *ResourceData for the config information. - // Guaranteed to never be nil. - ProvConfigDataKey = contextKey("provider config data") - - // This returns a terraform.UIOutput. Guaranteed to never be nil. - ProvOutputKey = contextKey("provider output") - - // This returns the raw InstanceState passed to Apply. Guaranteed to - // be set, but may be nil. - ProvRawStateKey = contextKey("provider raw state") -) - -// InternalValidate should be called to validate the structure -// of the provisioner. -// -// This should be called in a unit test to verify before release that this -// structure is properly configured for use. -func (p *Provisioner) InternalValidate() error { - if p == nil { - return errors.New("provisioner is nil") - } - - var validationErrors error - { - sm := schemaMap(p.ConnSchema) - if err := sm.InternalValidate(sm); err != nil { - validationErrors = multierror.Append(validationErrors, err) - } - } - - { - sm := schemaMap(p.Schema) - if err := sm.InternalValidate(sm); err != nil { - validationErrors = multierror.Append(validationErrors, err) - } - } - - if p.ApplyFunc == nil { - validationErrors = multierror.Append(validationErrors, fmt.Errorf( - "ApplyFunc must not be nil")) - } - - return validationErrors -} - -// StopContext returns a context that checks whether a provisioner is stopped. -func (p *Provisioner) StopContext() context.Context { - p.stopOnce.Do(p.stopInit) - return p.stopCtx -} - -func (p *Provisioner) stopInit() { - p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background()) -} - -// Stop implementation of terraform.ResourceProvisioner interface. -func (p *Provisioner) Stop() error { - p.stopOnce.Do(p.stopInit) - p.stopCtxCancel() - return nil -} - -// GetConfigSchema implementation of terraform.ResourceProvisioner interface. -func (p *Provisioner) GetConfigSchema() (*configschema.Block, error) { - return schemaMap(p.Schema).CoreConfigSchema(), nil -} - -// Apply implementation of terraform.ResourceProvisioner interface. -func (p *Provisioner) Apply( - o terraform.UIOutput, - s *terraform.InstanceState, - c *terraform.ResourceConfig) error { - var connData, configData *ResourceData - - { - // We first need to turn the connection information into a - // terraform.ResourceConfig so that we can use that type to more - // easily build a ResourceData structure. We do this by simply treating - // the conn info as configuration input. - raw := make(map[string]interface{}) - if s != nil { - for k, v := range s.Ephemeral.ConnInfo { - raw[k] = v - } - } - - c := terraform.NewResourceConfigRaw(raw) - sm := schemaMap(p.ConnSchema) - diff, err := sm.Diff(nil, c, nil, nil, true) - if err != nil { - return err - } - connData, err = sm.Data(nil, diff) - if err != nil { - return err - } - } - - { - // Build the configuration data. Doing this requires making a "diff" - // even though that's never used. We use that just to get the correct types. - configMap := schemaMap(p.Schema) - diff, err := configMap.Diff(nil, c, nil, nil, true) - if err != nil { - return err - } - configData, err = configMap.Data(nil, diff) - if err != nil { - return err - } - } - - // Build the context and call the function - ctx := p.StopContext() - ctx = context.WithValue(ctx, ProvConnDataKey, connData) - ctx = context.WithValue(ctx, ProvConfigDataKey, configData) - ctx = context.WithValue(ctx, ProvOutputKey, o) - ctx = context.WithValue(ctx, ProvRawStateKey, s) - return p.ApplyFunc(ctx) -} - -// Validate implements the terraform.ResourceProvisioner interface. -func (p *Provisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { - if err := p.InternalValidate(); err != nil { - return nil, []error{fmt.Errorf( - "Internal validation of the provisioner failed! This is always a bug\n"+ - "with the provisioner itself, and not a user issue. Please report\n"+ - "this bug:\n\n%s", err)} - } - - if p.Schema != nil { - w, e := schemaMap(p.Schema).Validate(c) - ws = append(ws, w...) - es = append(es, e...) - } - - if p.ValidateFunc != nil { - w, e := p.ValidateFunc(c) - ws = append(ws, w...) - es = append(es, e...) - } - - return ws, es -} diff --git a/helper/schema/provisioner_test.go b/helper/schema/provisioner_test.go deleted file mode 100644 index bac6610d3..000000000 --- a/helper/schema/provisioner_test.go +++ /dev/null @@ -1,334 +0,0 @@ -package schema - -import ( - "context" - "fmt" - "reflect" - "testing" - "time" - - "github.com/hashicorp/terraform/terraform" -) - -func TestProvisioner_impl(t *testing.T) { - var _ terraform.ResourceProvisioner = new(Provisioner) -} - -func noopApply(ctx context.Context) error { - return nil -} - -func TestProvisionerValidate(t *testing.T) { - cases := []struct { - Name string - P *Provisioner - Config map[string]interface{} - Err bool - Warns []string - }{ - { - Name: "No ApplyFunc", - P: &Provisioner{}, - Config: nil, - Err: true, - }, - { - Name: "Incorrect schema", - P: &Provisioner{ - Schema: map[string]*Schema{ - "foo": {}, - }, - ApplyFunc: noopApply, - }, - Config: nil, - Err: true, - }, - { - "Basic required field", - &Provisioner{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Required: true, - Type: TypeString, - }, - }, - ApplyFunc: noopApply, - }, - nil, - true, - nil, - }, - - { - "Basic required field set", - &Provisioner{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Required: true, - Type: TypeString, - }, - }, - ApplyFunc: noopApply, - }, - map[string]interface{}{ - "foo": "bar", - }, - false, - nil, - }, - { - Name: "Warning from property validation", - P: &Provisioner{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeString, - Optional: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - ws = append(ws, "Simple warning from property validation") - return - }, - }, - }, - ApplyFunc: noopApply, - }, - Config: map[string]interface{}{ - "foo": "", - }, - Err: false, - Warns: []string{"Simple warning from property validation"}, - }, - { - Name: "No schema", - P: &Provisioner{ - Schema: nil, - ApplyFunc: noopApply, - }, - Config: nil, - Err: false, - }, - { - Name: "Warning from provisioner ValidateFunc", - P: &Provisioner{ - Schema: nil, - ApplyFunc: noopApply, - ValidateFunc: func(*terraform.ResourceConfig) (ws []string, errors []error) { - ws = append(ws, "Simple warning from provisioner ValidateFunc") - return - }, - }, - Config: nil, - Err: false, - Warns: []string{"Simple warning from provisioner ValidateFunc"}, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - c := terraform.NewResourceConfigRaw(tc.Config) - ws, es := tc.P.Validate(c) - if len(es) > 0 != tc.Err { - t.Fatalf("%d: %#v %s", i, es, es) - } - if (tc.Warns != nil || len(ws) != 0) && !reflect.DeepEqual(ws, tc.Warns) { - t.Fatalf("%d: warnings mismatch, actual: %#v", i, ws) - } - }) - } -} - -func TestProvisionerApply(t *testing.T) { - cases := []struct { - Name string - P *Provisioner - Conn map[string]string - Config map[string]interface{} - Err bool - }{ - { - "Basic config", - &Provisioner{ - ConnSchema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ApplyFunc: func(ctx context.Context) error { - cd := ctx.Value(ProvConnDataKey).(*ResourceData) - d := ctx.Value(ProvConfigDataKey).(*ResourceData) - if d.Get("foo").(int) != 42 { - return fmt.Errorf("bad config data") - } - if cd.Get("foo").(string) != "bar" { - return fmt.Errorf("bad conn data") - } - - return nil - }, - }, - map[string]string{ - "foo": "bar", - }, - map[string]interface{}{ - "foo": 42, - }, - false, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - c := terraform.NewResourceConfigRaw(tc.Config) - - state := &terraform.InstanceState{ - Ephemeral: terraform.EphemeralState{ - ConnInfo: tc.Conn, - }, - } - - err := tc.P.Apply(nil, state, c) - if err != nil != tc.Err { - t.Fatalf("%d: %s", i, err) - } - }) - } -} - -func TestProvisionerApply_nilState(t *testing.T) { - p := &Provisioner{ - ConnSchema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ApplyFunc: func(ctx context.Context) error { - return nil - }, - } - - conf := map[string]interface{}{ - "foo": 42, - } - - c := terraform.NewResourceConfigRaw(conf) - err := p.Apply(nil, nil, c) - if err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestProvisionerStop(t *testing.T) { - var p Provisioner - - // Verify stopch blocks - ch := p.StopContext().Done() - select { - case <-ch: - t.Fatal("should not be stopped") - case <-time.After(10 * time.Millisecond): - } - - // Stop it - if err := p.Stop(); err != nil { - t.Fatalf("err: %s", err) - } - - select { - case <-ch: - case <-time.After(10 * time.Millisecond): - t.Fatal("should be stopped") - } -} - -func TestProvisionerStop_apply(t *testing.T) { - p := &Provisioner{ - ConnSchema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ApplyFunc: func(ctx context.Context) error { - <-ctx.Done() - return nil - }, - } - - conn := map[string]string{ - "foo": "bar", - } - - conf := map[string]interface{}{ - "foo": 42, - } - - c := terraform.NewResourceConfigRaw(conf) - state := &terraform.InstanceState{ - Ephemeral: terraform.EphemeralState{ - ConnInfo: conn, - }, - } - - // Run the apply in a goroutine - doneCh := make(chan struct{}) - go func() { - p.Apply(nil, state, c) - close(doneCh) - }() - - // Should block - select { - case <-doneCh: - t.Fatal("should not be done") - case <-time.After(10 * time.Millisecond): - } - - // Stop! - p.Stop() - - select { - case <-doneCh: - case <-time.After(10 * time.Millisecond): - t.Fatal("should be done") - } -} - -func TestProvisionerStop_stopFirst(t *testing.T) { - var p Provisioner - - // Stop it - if err := p.Stop(); err != nil { - t.Fatalf("err: %s", err) - } - - select { - case <-p.StopContext().Done(): - case <-time.After(10 * time.Millisecond): - t.Fatal("should be stopped") - } -} diff --git a/helper/schema/resource.go b/helper/schema/resource.go deleted file mode 100644 index dcfb32aea..000000000 --- a/helper/schema/resource.go +++ /dev/null @@ -1,842 +0,0 @@ -package schema - -import ( - "errors" - "fmt" - "log" - "strconv" - - "github.com/hashicorp/terraform/terraform" - "github.com/zclconf/go-cty/cty" -) - -var ReservedDataSourceFields = []string{ - "connection", - "count", - "depends_on", - "lifecycle", - "provider", - "provisioner", -} - -var ReservedResourceFields = []string{ - "connection", - "count", - "depends_on", - "id", - "lifecycle", - "provider", - "provisioner", -} - -// Resource represents a thing in Terraform that has a set of configurable -// attributes and a lifecycle (create, read, update, delete). -// -// The Resource schema is an abstraction that allows provider writers to -// worry only about CRUD operations while off-loading validation, diff -// generation, etc. to this higher level library. -// -// In spite of the name, this struct is not used only for terraform resources, -// but also for data sources. In the case of data sources, the Create, -// Update and Delete functions must not be provided. -type Resource struct { - // Schema is the schema for the configuration of this resource. - // - // The keys of this map are the configuration keys, and the values - // describe the schema of the configuration value. - // - // The schema is used to represent both configurable data as well - // as data that might be computed in the process of creating this - // resource. - Schema map[string]*Schema - - // SchemaVersion is the version number for this resource's Schema - // definition. The current SchemaVersion stored in the state for each - // resource. Provider authors can increment this version number - // when Schema semantics change. If the State's SchemaVersion is less than - // the current SchemaVersion, the InstanceState is yielded to the - // MigrateState callback, where the provider can make whatever changes it - // needs to update the state to be compatible to the latest version of the - // Schema. - // - // When unset, SchemaVersion defaults to 0, so provider authors can start - // their Versioning at any integer >= 1 - SchemaVersion int - - // MigrateState is deprecated and any new changes to a resource's schema - // should be handled by StateUpgraders. Existing MigrateState implementations - // should remain for compatibility with existing state. MigrateState will - // still be called if the stored SchemaVersion is less than the - // first version of the StateUpgraders. - // - // MigrateState is responsible for updating an InstanceState with an old - // version to the format expected by the current version of the Schema. - // - // It is called during Refresh if the State's stored SchemaVersion is less - // than the current SchemaVersion of the Resource. - // - // The function is yielded the state's stored SchemaVersion and a pointer to - // the InstanceState that needs updating, as well as the configured - // provider's configured meta interface{}, in case the migration process - // needs to make any remote API calls. - MigrateState StateMigrateFunc - - // StateUpgraders contains the functions responsible for upgrading an - // existing state with an old schema version to a newer schema. It is - // called specifically by Terraform when the stored schema version is less - // than the current SchemaVersion of the Resource. - // - // StateUpgraders map specific schema versions to a StateUpgrader - // function. The registered versions are expected to be ordered, - // consecutive values. The initial value may be greater than 0 to account - // for legacy schemas that weren't recorded and can be handled by - // MigrateState. - StateUpgraders []StateUpgrader - - // The functions below are the CRUD operations for this resource. - // - // The only optional operation is Update. If Update is not implemented, - // then updates will not be supported for this resource. - // - // The ResourceData parameter in the functions below are used to - // query configuration and changes for the resource as well as to set - // the ID, computed data, etc. - // - // The interface{} parameter is the result of the ConfigureFunc in - // the provider for this resource. If the provider does not define - // a ConfigureFunc, this will be nil. This parameter should be used - // to store API clients, configuration structures, etc. - // - // If any errors occur during each of the operation, an error should be - // returned. If a resource was partially updated, be careful to enable - // partial state mode for ResourceData and use it accordingly. - // - // Exists is a function that is called to check if a resource still - // exists. If this returns false, then this will affect the diff - // accordingly. If this function isn't set, it will not be called. You - // can also signal existence in the Read method by calling d.SetId("") - // if the Resource is no longer present and should be removed from state. - // The *ResourceData passed to Exists should _not_ be modified. - Create CreateFunc - Read ReadFunc - Update UpdateFunc - Delete DeleteFunc - Exists ExistsFunc - - // CustomizeDiff is a custom function for working with the diff that - // Terraform has created for this resource - it can be used to customize the - // diff that has been created, diff values not controlled by configuration, - // or even veto the diff altogether and abort the plan. It is passed a - // *ResourceDiff, a structure similar to ResourceData but lacking most write - // functions like Set, while introducing new functions that work with the - // diff such as SetNew, SetNewComputed, and ForceNew. - // - // The phases Terraform runs this in, and the state available via functions - // like Get and GetChange, are as follows: - // - // * New resource: One run with no state - // * Existing resource: One run with state - // * Existing resource, forced new: One run with state (before ForceNew), - // then one run without state (as if new resource) - // * Tainted resource: No runs (custom diff logic is skipped) - // * Destroy: No runs (standard diff logic is skipped on destroy diffs) - // - // This function needs to be resilient to support all scenarios. - // - // If this function needs to access external API resources, remember to flag - // the RequiresRefresh attribute mentioned below to ensure that - // -refresh=false is blocked when running plan or apply, as this means that - // this resource requires refresh-like behaviour to work effectively. - // - // For the most part, only computed fields can be customized by this - // function. - // - // This function is only allowed on regular resources (not data sources). - CustomizeDiff CustomizeDiffFunc - - // Importer is the ResourceImporter implementation for this resource. - // If this is nil, then this resource does not support importing. If - // this is non-nil, then it supports importing and ResourceImporter - // must be validated. The validity of ResourceImporter is verified - // by InternalValidate on Resource. - Importer *ResourceImporter - - // If non-empty, this string is emitted as a warning during Validate. - DeprecationMessage string - - // Timeouts allow users to specify specific time durations in which an - // operation should time out, to allow them to extend an action to suit their - // usage. For example, a user may specify a large Creation timeout for their - // AWS RDS Instance due to it's size, or restoring from a snapshot. - // Resource implementors must enable Timeout support by adding the allowed - // actions (Create, Read, Update, Delete, Default) to the Resource struct, and - // accessing them in the matching methods. - Timeouts *ResourceTimeout -} - -// ShimInstanceStateFromValue converts a cty.Value to a -// terraform.InstanceState. -func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.InstanceState, error) { - // Get the raw shimmed value. While this is correct, the set hashes don't - // match those from the Schema. - s := terraform.NewInstanceStateShimmedFromValue(state, r.SchemaVersion) - - // We now rebuild the state through the ResourceData, so that the set indexes - // match what helper/schema expects. - data, err := schemaMap(r.Schema).Data(s, nil) - if err != nil { - return nil, err - } - - s = data.State() - if s == nil { - s = &terraform.InstanceState{} - } - return s, nil -} - -// See Resource documentation. -type CreateFunc func(*ResourceData, interface{}) error - -// See Resource documentation. -type ReadFunc func(*ResourceData, interface{}) error - -// See Resource documentation. -type UpdateFunc func(*ResourceData, interface{}) error - -// See Resource documentation. -type DeleteFunc func(*ResourceData, interface{}) error - -// See Resource documentation. -type ExistsFunc func(*ResourceData, interface{}) (bool, error) - -// See Resource documentation. -type StateMigrateFunc func( - int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) - -type StateUpgrader struct { - // Version is the version schema that this Upgrader will handle, converting - // it to Version+1. - Version int - - // Type describes the schema that this function can upgrade. Type is - // required to decode the schema if the state was stored in a legacy - // flatmap format. - Type cty.Type - - // Upgrade takes the JSON encoded state and the provider meta value, and - // upgrades the state one single schema version. The provided state is - // deocded into the default json types using a map[string]interface{}. It - // is up to the StateUpgradeFunc to ensure that the returned value can be - // encoded using the new schema. - Upgrade StateUpgradeFunc -} - -// See StateUpgrader -type StateUpgradeFunc func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) - -// See Resource documentation. -type CustomizeDiffFunc func(*ResourceDiff, interface{}) error - -// Apply creates, updates, and/or deletes a resource. -func (r *Resource) Apply( - s *terraform.InstanceState, - d *terraform.InstanceDiff, - meta interface{}) (*terraform.InstanceState, error) { - data, err := schemaMap(r.Schema).Data(s, d) - if err != nil { - return s, err - } - if s != nil && data != nil { - data.providerMeta = s.ProviderMeta - } - - // Instance Diff shoould have the timeout info, need to copy it over to the - // ResourceData meta - rt := ResourceTimeout{} - if _, ok := d.Meta[TimeoutKey]; ok { - if err := rt.DiffDecode(d); err != nil { - log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) - } - } else if s != nil { - if _, ok := s.Meta[TimeoutKey]; ok { - if err := rt.StateDecode(s); err != nil { - log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) - } - } - } else { - log.Printf("[DEBUG] No meta timeoutkey found in Apply()") - } - data.timeouts = &rt - - if s == nil { - // The Terraform API dictates that this should never happen, but - // it doesn't hurt to be safe in this case. - s = new(terraform.InstanceState) - } - - if d.Destroy || d.RequiresNew() { - if s.ID != "" { - // Destroy the resource since it is created - if err := r.Delete(data, meta); err != nil { - return r.recordCurrentSchemaVersion(data.State()), err - } - - // Make sure the ID is gone. - data.SetId("") - } - - // If we're only destroying, and not creating, then return - // now since we're done! - if !d.RequiresNew() { - return nil, nil - } - - // Reset the data to be stateless since we just destroyed - data, err = schemaMap(r.Schema).Data(nil, d) - // data was reset, need to re-apply the parsed timeouts - data.timeouts = &rt - if err != nil { - return nil, err - } - } - - err = nil - if data.Id() == "" { - // We're creating, it is a new resource. - data.MarkNewResource() - err = r.Create(data, meta) - } else { - if r.Update == nil { - return s, fmt.Errorf("doesn't support update") - } - - err = r.Update(data, meta) - } - - return r.recordCurrentSchemaVersion(data.State()), err -} - -// Diff returns a diff of this resource. -func (r *Resource) Diff( - s *terraform.InstanceState, - c *terraform.ResourceConfig, - meta interface{}) (*terraform.InstanceDiff, error) { - - t := &ResourceTimeout{} - err := t.ConfigDecode(r, c) - - if err != nil { - return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err) - } - - instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, true) - if err != nil { - return instanceDiff, err - } - - if instanceDiff != nil { - if err := t.DiffEncode(instanceDiff); err != nil { - log.Printf("[ERR] Error encoding timeout to instance diff: %s", err) - } - } else { - log.Printf("[DEBUG] Instance Diff is nil in Diff()") - } - - return instanceDiff, err -} - -func (r *Resource) simpleDiff( - s *terraform.InstanceState, - c *terraform.ResourceConfig, - meta interface{}) (*terraform.InstanceDiff, error) { - - instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, false) - if err != nil { - return instanceDiff, err - } - - if instanceDiff == nil { - instanceDiff = terraform.NewInstanceDiff() - } - - // Make sure the old value is set in each of the instance diffs. - // This was done by the RequiresNew logic in the full legacy Diff. - for k, attr := range instanceDiff.Attributes { - if attr == nil { - continue - } - if s != nil { - attr.Old = s.Attributes[k] - } - } - - return instanceDiff, nil -} - -// Validate validates the resource configuration against the schema. -func (r *Resource) Validate(c *terraform.ResourceConfig) ([]string, []error) { - warns, errs := schemaMap(r.Schema).Validate(c) - - if r.DeprecationMessage != "" { - warns = append(warns, r.DeprecationMessage) - } - - return warns, errs -} - -// ReadDataApply loads the data for a data source, given a diff that -// describes the configuration arguments and desired computed attributes. -func (r *Resource) ReadDataApply( - d *terraform.InstanceDiff, - meta interface{}, -) (*terraform.InstanceState, error) { - // Data sources are always built completely from scratch - // on each read, so the source state is always nil. - data, err := schemaMap(r.Schema).Data(nil, d) - if err != nil { - return nil, err - } - - err = r.Read(data, meta) - state := data.State() - if state != nil && state.ID == "" { - // Data sources can set an ID if they want, but they aren't - // required to; we'll provide a placeholder if they don't, - // to preserve the invariant that all resources have non-empty - // ids. - state.ID = "-" - } - - return r.recordCurrentSchemaVersion(state), err -} - -// RefreshWithoutUpgrade reads the instance state, but does not call -// MigrateState or the StateUpgraders, since those are now invoked in a -// separate API call. -// RefreshWithoutUpgrade is part of the new plugin shims. -func (r *Resource) RefreshWithoutUpgrade( - s *terraform.InstanceState, - meta interface{}) (*terraform.InstanceState, error) { - // If the ID is already somehow blank, it doesn't exist - if s.ID == "" { - return nil, nil - } - - rt := ResourceTimeout{} - if _, ok := s.Meta[TimeoutKey]; ok { - if err := rt.StateDecode(s); err != nil { - log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) - } - } - - if r.Exists != nil { - // Make a copy of data so that if it is modified it doesn't - // affect our Read later. - data, err := schemaMap(r.Schema).Data(s, nil) - data.timeouts = &rt - - if err != nil { - return s, err - } - - if s != nil { - data.providerMeta = s.ProviderMeta - } - - exists, err := r.Exists(data, meta) - if err != nil { - return s, err - } - if !exists { - return nil, nil - } - } - - data, err := schemaMap(r.Schema).Data(s, nil) - data.timeouts = &rt - if err != nil { - return s, err - } - - if s != nil { - data.providerMeta = s.ProviderMeta - } - - err = r.Read(data, meta) - state := data.State() - if state != nil && state.ID == "" { - state = nil - } - - return r.recordCurrentSchemaVersion(state), err -} - -// Refresh refreshes the state of the resource. -func (r *Resource) Refresh( - s *terraform.InstanceState, - meta interface{}) (*terraform.InstanceState, error) { - // If the ID is already somehow blank, it doesn't exist - if s.ID == "" { - return nil, nil - } - - rt := ResourceTimeout{} - if _, ok := s.Meta[TimeoutKey]; ok { - if err := rt.StateDecode(s); err != nil { - log.Printf("[ERR] Error decoding ResourceTimeout: %s", err) - } - } - - if r.Exists != nil { - // Make a copy of data so that if it is modified it doesn't - // affect our Read later. - data, err := schemaMap(r.Schema).Data(s, nil) - data.timeouts = &rt - - if err != nil { - return s, err - } - - exists, err := r.Exists(data, meta) - if err != nil { - return s, err - } - if !exists { - return nil, nil - } - } - - // there may be new StateUpgraders that need to be run - s, err := r.upgradeState(s, meta) - if err != nil { - return s, err - } - - data, err := schemaMap(r.Schema).Data(s, nil) - data.timeouts = &rt - if err != nil { - return s, err - } - - err = r.Read(data, meta) - state := data.State() - if state != nil && state.ID == "" { - state = nil - } - - return r.recordCurrentSchemaVersion(state), err -} - -func (r *Resource) upgradeState(s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { - var err error - - needsMigration, stateSchemaVersion := r.checkSchemaVersion(s) - migrate := needsMigration && r.MigrateState != nil - - if migrate { - s, err = r.MigrateState(stateSchemaVersion, s, meta) - if err != nil { - return s, err - } - } - - if len(r.StateUpgraders) == 0 { - return s, nil - } - - // If we ran MigrateState, then the stateSchemaVersion value is no longer - // correct. We can expect the first upgrade function to be the correct - // schema type version. - if migrate { - stateSchemaVersion = r.StateUpgraders[0].Version - } - - schemaType := r.CoreConfigSchema().ImpliedType() - // find the expected type to convert the state - for _, upgrader := range r.StateUpgraders { - if stateSchemaVersion == upgrader.Version { - schemaType = upgrader.Type - } - } - - // StateUpgraders only operate on the new JSON format state, so the state - // need to be converted. - stateVal, err := StateValueFromInstanceState(s, schemaType) - if err != nil { - return nil, err - } - - jsonState, err := StateValueToJSONMap(stateVal, schemaType) - if err != nil { - return nil, err - } - - for _, upgrader := range r.StateUpgraders { - if stateSchemaVersion != upgrader.Version { - continue - } - - jsonState, err = upgrader.Upgrade(jsonState, meta) - if err != nil { - return nil, err - } - stateSchemaVersion++ - } - - // now we need to re-flatmap the new state - stateVal, err = JSONMapToStateValue(jsonState, r.CoreConfigSchema()) - if err != nil { - return nil, err - } - - return r.ShimInstanceStateFromValue(stateVal) -} - -// InternalValidate should be called to validate the structure -// of the resource. -// -// This should be called in a unit test for any resource to verify -// before release that a resource is properly configured for use with -// this library. -// -// Provider.InternalValidate() will automatically call this for all of -// the resources it manages, so you don't need to call this manually if it -// is part of a Provider. -func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error { - if r == nil { - return errors.New("resource is nil") - } - - if !writable { - if r.Create != nil || r.Update != nil || r.Delete != nil { - return fmt.Errorf("must not implement Create, Update or Delete") - } - - // CustomizeDiff cannot be defined for read-only resources - if r.CustomizeDiff != nil { - return fmt.Errorf("cannot implement CustomizeDiff") - } - } - - tsm := topSchemaMap - - if r.isTopLevel() && writable { - // All non-Computed attributes must be ForceNew if Update is not defined - if r.Update == nil { - nonForceNewAttrs := make([]string, 0) - for k, v := range r.Schema { - if !v.ForceNew && !v.Computed { - nonForceNewAttrs = append(nonForceNewAttrs, k) - } - } - if len(nonForceNewAttrs) > 0 { - return fmt.Errorf( - "No Update defined, must set ForceNew on: %#v", nonForceNewAttrs) - } - } else { - nonUpdateableAttrs := make([]string, 0) - for k, v := range r.Schema { - if v.ForceNew || v.Computed && !v.Optional { - nonUpdateableAttrs = append(nonUpdateableAttrs, k) - } - } - updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs) - if updateableAttrs == 0 { - return fmt.Errorf( - "All fields are ForceNew or Computed w/out Optional, Update is superfluous") - } - } - - tsm = schemaMap(r.Schema) - - // Destroy, and Read are required - if r.Read == nil { - return fmt.Errorf("Read must be implemented") - } - if r.Delete == nil { - return fmt.Errorf("Delete must be implemented") - } - - // If we have an importer, we need to verify the importer. - if r.Importer != nil { - if err := r.Importer.InternalValidate(); err != nil { - return err - } - } - - for k, f := range tsm { - if isReservedResourceFieldName(k, f) { - return fmt.Errorf("%s is a reserved field name", k) - } - } - } - - lastVersion := -1 - for _, u := range r.StateUpgraders { - if lastVersion >= 0 && u.Version-lastVersion > 1 { - return fmt.Errorf("missing schema version between %d and %d", lastVersion, u.Version) - } - - if u.Version >= r.SchemaVersion { - return fmt.Errorf("StateUpgrader version %d is >= current version %d", u.Version, r.SchemaVersion) - } - - if !u.Type.IsObjectType() { - return fmt.Errorf("StateUpgrader %d type is not cty.Object", u.Version) - } - - if u.Upgrade == nil { - return fmt.Errorf("StateUpgrader %d missing StateUpgradeFunc", u.Version) - } - - lastVersion = u.Version - } - - if lastVersion >= 0 && lastVersion != r.SchemaVersion-1 { - return fmt.Errorf("missing StateUpgrader between %d and %d", lastVersion, r.SchemaVersion) - } - - // Data source - if r.isTopLevel() && !writable { - tsm = schemaMap(r.Schema) - for k, _ := range tsm { - if isReservedDataSourceFieldName(k) { - return fmt.Errorf("%s is a reserved field name", k) - } - } - } - - return schemaMap(r.Schema).InternalValidate(tsm) -} - -func isReservedDataSourceFieldName(name string) bool { - for _, reservedName := range ReservedDataSourceFields { - if name == reservedName { - return true - } - } - return false -} - -func isReservedResourceFieldName(name string, s *Schema) bool { - // Allow phasing out "id" - // See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415 - if name == "id" && (s.Deprecated != "" || s.Removed != "") { - return false - } - - for _, reservedName := range ReservedResourceFields { - if name == reservedName { - return true - } - } - return false -} - -// Data returns a ResourceData struct for this Resource. Each return value -// is a separate copy and can be safely modified differently. -// -// The data returned from this function has no actual affect on the Resource -// itself (including the state given to this function). -// -// This function is useful for unit tests and ResourceImporter functions. -func (r *Resource) Data(s *terraform.InstanceState) *ResourceData { - result, err := schemaMap(r.Schema).Data(s, nil) - if err != nil { - // At the time of writing, this isn't possible (Data never returns - // non-nil errors). We panic to find this in the future if we have to. - // I don't see a reason for Data to ever return an error. - panic(err) - } - - // load the Resource timeouts - result.timeouts = r.Timeouts - if result.timeouts == nil { - result.timeouts = &ResourceTimeout{} - } - - // Set the schema version to latest by default - result.meta = map[string]interface{}{ - "schema_version": strconv.Itoa(r.SchemaVersion), - } - - return result -} - -// TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing -// -// TODO: May be able to be removed with the above ResourceData function. -func (r *Resource) TestResourceData() *ResourceData { - return &ResourceData{ - schema: r.Schema, - } -} - -// SchemasForFlatmapPath tries its best to find a sequence of schemas that -// the given dot-delimited attribute path traverses through in the schema -// of the receiving Resource. -func (r *Resource) SchemasForFlatmapPath(path string) []*Schema { - return SchemasForFlatmapPath(path, r.Schema) -} - -// Returns true if the resource is "top level" i.e. not a sub-resource. -func (r *Resource) isTopLevel() bool { - // TODO: This is a heuristic; replace with a definitive attribute? - return (r.Create != nil || r.Read != nil) -} - -// Determines if a given InstanceState needs to be migrated by checking the -// stored version number with the current SchemaVersion -func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { - // Get the raw interface{} value for the schema version. If it doesn't - // exist or is nil then set it to zero. - raw := is.Meta["schema_version"] - if raw == nil { - raw = "0" - } - - // Try to convert it to a string. If it isn't a string then we pretend - // that it isn't set at all. It should never not be a string unless it - // was manually tampered with. - rawString, ok := raw.(string) - if !ok { - rawString = "0" - } - - stateSchemaVersion, _ := strconv.Atoi(rawString) - - // Don't run MigrateState if the version is handled by a StateUpgrader, - // since StateMigrateFuncs are not required to handle unknown versions - maxVersion := r.SchemaVersion - if len(r.StateUpgraders) > 0 { - maxVersion = r.StateUpgraders[0].Version - } - - return stateSchemaVersion < maxVersion, stateSchemaVersion -} - -func (r *Resource) recordCurrentSchemaVersion( - state *terraform.InstanceState) *terraform.InstanceState { - if state != nil && r.SchemaVersion > 0 { - if state.Meta == nil { - state.Meta = make(map[string]interface{}) - } - state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) - } - return state -} - -// Noop is a convenience implementation of resource function which takes -// no action and returns no error. -func Noop(*ResourceData, interface{}) error { - return nil -} - -// RemoveFromState is a convenience implementation of a resource function -// which sets the resource ID to empty string (to remove it from state) -// and returns no error. -func RemoveFromState(d *ResourceData, _ interface{}) error { - d.SetId("") - return nil -} diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go deleted file mode 100644 index fb9387e29..000000000 --- a/helper/schema/resource_data.go +++ /dev/null @@ -1,561 +0,0 @@ -package schema - -import ( - "log" - "reflect" - "strings" - "sync" - "time" - - "github.com/hashicorp/terraform/terraform" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/gocty" -) - -// ResourceData is used to query and set the attributes of a resource. -// -// ResourceData is the primary argument received for CRUD operations on -// a resource as well as configuration of a provider. It is a powerful -// structure that can be used to not only query data, but check for changes, -// define partial state updates, etc. -// -// The most relevant methods to take a look at are Get, Set, and Partial. -type ResourceData struct { - // Settable (internally) - schema map[string]*Schema - config *terraform.ResourceConfig - state *terraform.InstanceState - diff *terraform.InstanceDiff - meta map[string]interface{} - timeouts *ResourceTimeout - providerMeta cty.Value - - // Don't set - multiReader *MultiLevelFieldReader - setWriter *MapFieldWriter - newState *terraform.InstanceState - partial bool - partialMap map[string]struct{} - once sync.Once - isNew bool - - panicOnError bool -} - -// getResult is the internal structure that is generated when a Get -// is called that contains some extra data that might be used. -type getResult struct { - Value interface{} - ValueProcessed interface{} - Computed bool - Exists bool - Schema *Schema -} - -// UnsafeSetFieldRaw allows setting arbitrary values in state to arbitrary -// values, bypassing schema. This MUST NOT be used in normal circumstances - -// it exists only to support the remote_state data source. -// -// Deprecated: Fully define schema attributes and use Set() instead. -func (d *ResourceData) UnsafeSetFieldRaw(key string, value string) { - d.once.Do(d.init) - - d.setWriter.unsafeWriteField(key, value) -} - -// Get returns the data for the given key, or nil if the key doesn't exist -// in the schema. -// -// If the key does exist in the schema but doesn't exist in the configuration, -// then the default value for that type will be returned. For strings, this is -// "", for numbers it is 0, etc. -// -// If you want to test if something is set at all in the configuration, -// use GetOk. -func (d *ResourceData) Get(key string) interface{} { - v, _ := d.GetOk(key) - return v -} - -// GetChange returns the old and new value for a given key. -// -// HasChange should be used to check if a change exists. It is possible -// that both the old and new value are the same if the old value was not -// set and the new value is. This is common, for example, for boolean -// fields which have a zero value of false. -func (d *ResourceData) GetChange(key string) (interface{}, interface{}) { - o, n := d.getChange(key, getSourceState, getSourceDiff) - return o.Value, n.Value -} - -// GetOk returns the data for the given key and whether or not the key -// has been set to a non-zero value at some point. -// -// The first result will not necessarilly be nil if the value doesn't exist. -// The second result should be checked to determine this information. -func (d *ResourceData) GetOk(key string) (interface{}, bool) { - r := d.getRaw(key, getSourceSet) - exists := r.Exists && !r.Computed - if exists { - // If it exists, we also want to verify it is not the zero-value. - value := r.Value - zero := r.Schema.Type.Zero() - - if eq, ok := value.(Equal); ok { - exists = !eq.Equal(zero) - } else { - exists = !reflect.DeepEqual(value, zero) - } - } - - return r.Value, exists -} - -// GetOkExists returns the data for a given key and whether or not the key -// has been set to a non-zero value. This is only useful for determining -// if boolean attributes have been set, if they are Optional but do not -// have a Default value. -// -// This is nearly the same function as GetOk, yet it does not check -// for the zero value of the attribute's type. This allows for attributes -// without a default, to fully check for a literal assignment, regardless -// of the zero-value for that type. -// This should only be used if absolutely required/needed. -func (d *ResourceData) GetOkExists(key string) (interface{}, bool) { - r := d.getRaw(key, getSourceSet) - exists := r.Exists && !r.Computed - return r.Value, exists -} - -func (d *ResourceData) getRaw(key string, level getSource) getResult { - var parts []string - if key != "" { - parts = strings.Split(key, ".") - } - - return d.get(parts, level) -} - -// HasChange returns whether or not the given key has been changed. -func (d *ResourceData) HasChange(key string) bool { - o, n := d.GetChange(key) - - // If the type implements the Equal interface, then call that - // instead of just doing a reflect.DeepEqual. An example where this is - // needed is *Set - if eq, ok := o.(Equal); ok { - return !eq.Equal(n) - } - - return !reflect.DeepEqual(o, n) -} - -// Partial turns partial state mode on/off. -// -// When partial state mode is enabled, then only key prefixes specified -// by SetPartial will be in the final state. This allows providers to return -// partial states for partially applied resources (when errors occur). -func (d *ResourceData) Partial(on bool) { - d.partial = on - if on { - if d.partialMap == nil { - d.partialMap = make(map[string]struct{}) - } - } else { - d.partialMap = nil - } -} - -// Set sets the value for the given key. -// -// If the key is invalid or the value is not a correct type, an error -// will be returned. -func (d *ResourceData) Set(key string, value interface{}) error { - d.once.Do(d.init) - - // If the value is a pointer to a non-struct, get its value and - // use that. This allows Set to take a pointer to primitives to - // simplify the interface. - reflectVal := reflect.ValueOf(value) - if reflectVal.Kind() == reflect.Ptr { - if reflectVal.IsNil() { - // If the pointer is nil, then the value is just nil - value = nil - } else { - // Otherwise, we dereference the pointer as long as its not - // a pointer to a struct, since struct pointers are allowed. - reflectVal = reflect.Indirect(reflectVal) - if reflectVal.Kind() != reflect.Struct { - value = reflectVal.Interface() - } - } - } - - err := d.setWriter.WriteField(strings.Split(key, "."), value) - if err != nil && d.panicOnError { - panic(err) - } - return err -} - -// SetPartial adds the key to the final state output while -// in partial state mode. The key must be a root key in the schema (i.e. -// it cannot be "list.0"). -// -// If partial state mode is disabled, then this has no effect. Additionally, -// whenever partial state mode is toggled, the partial data is cleared. -func (d *ResourceData) SetPartial(k string) { - if d.partial { - d.partialMap[k] = struct{}{} - } -} - -func (d *ResourceData) MarkNewResource() { - d.isNew = true -} - -func (d *ResourceData) IsNewResource() bool { - return d.isNew -} - -// Id returns the ID of the resource. -func (d *ResourceData) Id() string { - var result string - - if d.state != nil { - result = d.state.ID - if result == "" { - result = d.state.Attributes["id"] - } - } - - if d.newState != nil { - result = d.newState.ID - if result == "" { - result = d.newState.Attributes["id"] - } - } - - return result -} - -// ConnInfo returns the connection info for this resource. -func (d *ResourceData) ConnInfo() map[string]string { - if d.newState != nil { - return d.newState.Ephemeral.ConnInfo - } - - if d.state != nil { - return d.state.Ephemeral.ConnInfo - } - - return nil -} - -// SetId sets the ID of the resource. If the value is blank, then the -// resource is destroyed. -func (d *ResourceData) SetId(v string) { - d.once.Do(d.init) - d.newState.ID = v - - // once we transition away from the legacy state types, "id" will no longer - // be a special field, and will become a normal attribute. - // set the attribute normally - d.setWriter.unsafeWriteField("id", v) - - // Make sure the newState is also set, otherwise the old value - // may get precedence. - if d.newState.Attributes == nil { - d.newState.Attributes = map[string]string{} - } - d.newState.Attributes["id"] = v -} - -// SetConnInfo sets the connection info for a resource. -func (d *ResourceData) SetConnInfo(v map[string]string) { - d.once.Do(d.init) - d.newState.Ephemeral.ConnInfo = v -} - -// SetType sets the ephemeral type for the data. This is only required -// for importing. -func (d *ResourceData) SetType(t string) { - d.once.Do(d.init) - d.newState.Ephemeral.Type = t -} - -// State returns the new InstanceState after the diff and any Set -// calls. -func (d *ResourceData) State() *terraform.InstanceState { - var result terraform.InstanceState - result.ID = d.Id() - result.Meta = d.meta - - // If we have no ID, then this resource doesn't exist and we just - // return nil. - if result.ID == "" { - return nil - } - - if d.timeouts != nil { - if err := d.timeouts.StateEncode(&result); err != nil { - log.Printf("[ERR] Error encoding Timeout meta to Instance State: %s", err) - } - } - - // Look for a magic key in the schema that determines we skip the - // integrity check of fields existing in the schema, allowing dynamic - // keys to be created. - hasDynamicAttributes := false - for k, _ := range d.schema { - if k == "__has_dynamic_attributes" { - hasDynamicAttributes = true - log.Printf("[INFO] Resource %s has dynamic attributes", result.ID) - } - } - - // In order to build the final state attributes, we read the full - // attribute set as a map[string]interface{}, write it to a MapFieldWriter, - // and then use that map. - rawMap := make(map[string]interface{}) - for k := range d.schema { - source := getSourceSet - if d.partial { - source = getSourceState - if _, ok := d.partialMap[k]; ok { - source = getSourceSet - } - } - - raw := d.get([]string{k}, source) - if raw.Exists && !raw.Computed { - rawMap[k] = raw.Value - if raw.ValueProcessed != nil { - rawMap[k] = raw.ValueProcessed - } - } - } - - mapW := &MapFieldWriter{Schema: d.schema} - if err := mapW.WriteField(nil, rawMap); err != nil { - log.Printf("[ERR] Error writing fields: %s", err) - return nil - } - - result.Attributes = mapW.Map() - - if hasDynamicAttributes { - // If we have dynamic attributes, just copy the attributes map - // one for one into the result attributes. - for k, v := range d.setWriter.Map() { - // Don't clobber schema values. This limits usage of dynamic - // attributes to names which _do not_ conflict with schema - // keys! - if _, ok := result.Attributes[k]; !ok { - result.Attributes[k] = v - } - } - } - - if d.newState != nil { - result.Ephemeral = d.newState.Ephemeral - } - - // TODO: This is hacky and we can remove this when we have a proper - // state writer. We should instead have a proper StateFieldWriter - // and use that. - for k, schema := range d.schema { - if schema.Type != TypeMap { - continue - } - - if result.Attributes[k] == "" { - delete(result.Attributes, k) - } - } - - if v := d.Id(); v != "" { - result.Attributes["id"] = d.Id() - } - - if d.state != nil { - result.Tainted = d.state.Tainted - } - - return &result -} - -// Timeout returns the data for the given timeout key -// Returns a duration of 20 minutes for any key not found, or not found and no default. -func (d *ResourceData) Timeout(key string) time.Duration { - key = strings.ToLower(key) - - // System default of 20 minutes - defaultTimeout := 20 * time.Minute - - if d.timeouts == nil { - return defaultTimeout - } - - var timeout *time.Duration - switch key { - case TimeoutCreate: - timeout = d.timeouts.Create - case TimeoutRead: - timeout = d.timeouts.Read - case TimeoutUpdate: - timeout = d.timeouts.Update - case TimeoutDelete: - timeout = d.timeouts.Delete - } - - if timeout != nil { - return *timeout - } - - if d.timeouts.Default != nil { - return *d.timeouts.Default - } - - return defaultTimeout -} - -func (d *ResourceData) init() { - // Initialize the field that will store our new state - var copyState terraform.InstanceState - if d.state != nil { - copyState = *d.state.DeepCopy() - } - d.newState = ©State - - // Initialize the map for storing set data - d.setWriter = &MapFieldWriter{Schema: d.schema} - - // Initialize the reader for getting data from the - // underlying sources (config, diff, etc.) - readers := make(map[string]FieldReader) - var stateAttributes map[string]string - if d.state != nil { - stateAttributes = d.state.Attributes - readers["state"] = &MapFieldReader{ - Schema: d.schema, - Map: BasicMapReader(stateAttributes), - } - } - if d.config != nil { - readers["config"] = &ConfigFieldReader{ - Schema: d.schema, - Config: d.config, - } - } - if d.diff != nil { - readers["diff"] = &DiffFieldReader{ - Schema: d.schema, - Diff: d.diff, - Source: &MultiLevelFieldReader{ - Levels: []string{"state", "config"}, - Readers: readers, - }, - } - } - readers["set"] = &MapFieldReader{ - Schema: d.schema, - Map: BasicMapReader(d.setWriter.Map()), - } - d.multiReader = &MultiLevelFieldReader{ - Levels: []string{ - "state", - "config", - "diff", - "set", - }, - - Readers: readers, - } -} - -func (d *ResourceData) diffChange( - k string) (interface{}, interface{}, bool, bool, bool) { - // Get the change between the state and the config. - o, n := d.getChange(k, getSourceState, getSourceConfig|getSourceExact) - if !o.Exists { - o.Value = nil - } - if !n.Exists { - n.Value = nil - } - - // Return the old, new, and whether there is a change - return o.Value, n.Value, !reflect.DeepEqual(o.Value, n.Value), n.Computed, false -} - -func (d *ResourceData) getChange( - k string, - oldLevel getSource, - newLevel getSource) (getResult, getResult) { - var parts, parts2 []string - if k != "" { - parts = strings.Split(k, ".") - parts2 = strings.Split(k, ".") - } - - o := d.get(parts, oldLevel) - n := d.get(parts2, newLevel) - return o, n -} - -func (d *ResourceData) get(addr []string, source getSource) getResult { - d.once.Do(d.init) - - level := "set" - flags := source & ^getSourceLevelMask - exact := flags&getSourceExact != 0 - source = source & getSourceLevelMask - if source >= getSourceSet { - level = "set" - } else if source >= getSourceDiff { - level = "diff" - } else if source >= getSourceConfig { - level = "config" - } else { - level = "state" - } - - var result FieldReadResult - var err error - if exact { - result, err = d.multiReader.ReadFieldExact(addr, level) - } else { - result, err = d.multiReader.ReadFieldMerge(addr, level) - } - if err != nil { - panic(err) - } - - // If the result doesn't exist, then we set the value to the zero value - var schema *Schema - if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 { - schema = schemaL[len(schemaL)-1] - } - - if result.Value == nil && schema != nil { - result.Value = result.ValueOrZero(schema) - } - - // Transform the FieldReadResult into a getResult. It might be worth - // merging these two structures one day. - return getResult{ - Value: result.Value, - ValueProcessed: result.ValueProcessed, - Computed: result.Computed, - Exists: result.Exists, - Schema: schema, - } -} - -func (d *ResourceData) GetProviderMeta(dst interface{}) error { - if d.providerMeta.IsNull() { - return nil - } - return gocty.FromCtyValue(d.providerMeta, &dst) -} diff --git a/helper/schema/resource_data_get_source.go b/helper/schema/resource_data_get_source.go deleted file mode 100644 index 8bfb079be..000000000 --- a/helper/schema/resource_data_get_source.go +++ /dev/null @@ -1,17 +0,0 @@ -package schema - -//go:generate go run golang.org/x/tools/cmd/stringer -type=getSource resource_data_get_source.go - -// getSource represents the level we want to get for a value (internally). -// Any source less than or equal to the level will be loaded (whichever -// has a value first). -type getSource byte - -const ( - getSourceState getSource = 1 << iota - getSourceConfig - getSourceDiff - getSourceSet - getSourceExact // Only get from the _exact_ level - getSourceLevelMask getSource = getSourceState | getSourceConfig | getSourceDiff | getSourceSet -) diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go deleted file mode 100644 index 5af0ff8e5..000000000 --- a/helper/schema/resource_data_test.go +++ /dev/null @@ -1,3564 +0,0 @@ -package schema - -import ( - "fmt" - "math" - "os" - "reflect" - "testing" - "time" - - "github.com/hashicorp/terraform/terraform" -) - -func TestResourceDataGet(t *testing.T) { - cases := []struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - Value interface{} - }{ - // #0 - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - NewComputed: true, - }, - }, - }, - - Key: "availability_zone", - Value: "", - }, - - // #1 - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Key: "availability_zone", - - Value: "foo", - }, - - // #2 - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo!", - NewExtra: "foo", - }, - }, - }, - - Key: "availability_zone", - Value: "foo", - }, - - // #3 - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "bar", - }, - }, - - Diff: nil, - - Key: "availability_zone", - - Value: "bar", - }, - - // #4 - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - NewComputed: true, - }, - }, - }, - - Key: "availability_zone", - Value: "", - }, - - // #5 - { - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeInt, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "port": "80", - }, - }, - - Diff: nil, - - Key: "port", - - Value: 80, - }, - - // #6 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.0": "1", - "ports.1": "2", - "ports.2": "5", - }, - }, - - Key: "ports.1", - - Value: 2, - }, - - // #7 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.0": "1", - "ports.1": "2", - "ports.2": "5", - }, - }, - - Key: "ports.#", - - Value: 3, - }, - - // #8 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Key: "ports.#", - - Value: 0, - }, - - // #9 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.0": "1", - "ports.1": "2", - "ports.2": "5", - }, - }, - - Key: "ports", - - Value: []interface{}{1, 2, 5}, - }, - - // #10 - { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ingress.#": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ingress.0.from": &terraform.ResourceAttrDiff{ - Old: "", - New: "8080", - }, - }, - }, - - Key: "ingress.0", - - Value: map[string]interface{}{ - "from": 8080, - }, - }, - - // #11 - { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ingress.#": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ingress.0.from": &terraform.ResourceAttrDiff{ - Old: "", - New: "8080", - }, - }, - }, - - Key: "ingress", - - Value: []interface{}{ - map[string]interface{}{ - "from": 8080, - }, - }, - }, - - // #12 Computed get - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - - Key: "availability_zone", - - Value: "foo", - }, - - // #13 Full object - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Key: "", - - Value: map[string]interface{}{ - "availability_zone": "foo", - }, - }, - - // #14 List of maps - { - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{ - Type: TypeMap, - }, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "2", - }, - "config_vars.0.foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - "config_vars.1.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - - Key: "config_vars", - - Value: []interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - map[string]interface{}{ - "bar": "baz", - }, - }, - }, - - // #15 List of maps in state - { - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{ - Type: TypeMap, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.#": "2", - "config_vars.0.foo": "baz", - "config_vars.1.bar": "bar", - }, - }, - - Diff: nil, - - Key: "config_vars", - - Value: []interface{}{ - map[string]interface{}{ - "foo": "baz", - }, - map[string]interface{}{ - "bar": "bar", - }, - }, - }, - - // #16 List of maps with removal in diff - { - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{ - Type: TypeMap, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.#": "1", - "config_vars.0.FOO": "bar", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "0", - }, - "config_vars.0.FOO": &terraform.ResourceAttrDiff{ - Old: "bar", - NewRemoved: true, - }, - }, - }, - - Key: "config_vars", - - Value: []interface{}{}, - }, - - // #17 Sets - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "1", - "ports.80": "80", - }, - }, - - Diff: nil, - - Key: "ports", - - Value: []interface{}{80}, - }, - - // #18 - { - Schema: map[string]*Schema{ - "data": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "value": &Schema{ - Type: TypeString, - Required: true, - }, - }, - }, - Set: func(a interface{}) int { - m := a.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "data.#": "1", - "data.10.index": "10", - "data.10.value": "50", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "data.10.value": &terraform.ResourceAttrDiff{ - Old: "50", - New: "80", - }, - }, - }, - - Key: "data", - - Value: []interface{}{ - map[string]interface{}{ - "index": 10, - "value": "80", - }, - }, - }, - - // #19 Empty Set - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports", - - Value: []interface{}{}, - }, - - // #20 Float zero - { - Schema: map[string]*Schema{ - "ratio": &Schema{ - Type: TypeFloat, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ratio", - - Value: 0.0, - }, - - // #21 Float given - { - Schema: map[string]*Schema{ - "ratio": &Schema{ - Type: TypeFloat, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ratio": "0.5", - }, - }, - - Diff: nil, - - Key: "ratio", - - Value: 0.5, - }, - - // #22 Float diff - { - Schema: map[string]*Schema{ - "ratio": &Schema{ - Type: TypeFloat, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ratio": "-0.5", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ratio": &terraform.ResourceAttrDiff{ - Old: "-0.5", - New: "33.0", - }, - }, - }, - - Key: "ratio", - - Value: 33.0, - }, - - // #23 Sets with removed elements - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "1", - "ports.80": "80", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "1", - }, - "ports.80": &terraform.ResourceAttrDiff{ - Old: "80", - New: "80", - }, - "ports.8080": &terraform.ResourceAttrDiff{ - Old: "8080", - New: "0", - NewRemoved: true, - }, - }, - }, - - Key: "ports", - - Value: []interface{}{80}, - }, - } - - for i, tc := range cases { - d, err := schemaMap(tc.Schema).Data(tc.State, tc.Diff) - if err != nil { - t.Fatalf("err: %s", err) - } - - v := d.Get(tc.Key) - if s, ok := v.(*Set); ok { - v = s.List() - } - - if !reflect.DeepEqual(v, tc.Value) { - t.Fatalf("Bad: %d\n\n%#v\n\nExpected: %#v", i, v, tc.Value) - } - } -} - -func TestResourceDataGetChange(t *testing.T) { - cases := []struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - OldValue interface{} - NewValue interface{} - }{ - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Key: "availability_zone", - - OldValue: "", - NewValue: "foo", - }, - - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Key: "availability_zone", - - OldValue: "foo", - NewValue: "foo", - }, - } - - for i, tc := range cases { - d, err := schemaMap(tc.Schema).Data(tc.State, tc.Diff) - if err != nil { - t.Fatalf("err: %s", err) - } - - o, n := d.GetChange(tc.Key) - if !reflect.DeepEqual(o, tc.OldValue) { - t.Fatalf("Old Bad: %d\n\n%#v", i, o) - } - if !reflect.DeepEqual(n, tc.NewValue) { - t.Fatalf("New Bad: %d\n\n%#v", i, n) - } - } -} - -func TestResourceDataGetOk(t *testing.T) { - cases := []struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - Value interface{} - Ok bool - }{ - /* - * Primitives - */ - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "", - }, - }, - }, - - Key: "availability_zone", - Value: "", - Ok: false, - }, - - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - - Key: "availability_zone", - Value: "", - Ok: false, - }, - - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "availability_zone", - Value: "", - Ok: false, - }, - - /* - * Lists - */ - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports", - Value: []interface{}{}, - Ok: false, - }, - - /* - * Map - */ - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeMap, - Optional: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports", - Value: map[string]interface{}{}, - Ok: false, - }, - - /* - * Set - */ - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { return a.(int) }, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports", - Value: []interface{}{}, - Ok: false, - }, - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { return a.(int) }, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports.0", - Value: 0, - Ok: false, - }, - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { return a.(int) }, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "0", - }, - }, - }, - - Key: "ports", - Value: []interface{}{}, - Ok: false, - }, - - // Further illustrates and clarifiies the GetOk semantics from #933, and - // highlights the limitation that zero-value config is currently - // indistinguishable from unset config. - { - Schema: map[string]*Schema{ - "from_port": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "from_port": &terraform.ResourceAttrDiff{ - Old: "", - New: "0", - }, - }, - }, - - Key: "from_port", - Value: 0, - Ok: false, - }, - } - - for i, tc := range cases { - d, err := schemaMap(tc.Schema).Data(tc.State, tc.Diff) - if err != nil { - t.Fatalf("err: %s", err) - } - - v, ok := d.GetOk(tc.Key) - if s, ok := v.(*Set); ok { - v = s.List() - } - - if !reflect.DeepEqual(v, tc.Value) { - t.Fatalf("Bad: %d\n\n%#v", i, v) - } - if ok != tc.Ok { - t.Fatalf("%d: expected ok: %t, got: %t", i, tc.Ok, ok) - } - } -} - -func TestResourceDataGetOkExists(t *testing.T) { - cases := []struct { - Name string - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - Value interface{} - Ok bool - }{ - /* - * Primitives - */ - { - Name: "string-literal-empty", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "", - }, - }, - }, - - Key: "availability_zone", - Value: "", - Ok: true, - }, - - { - Name: "string-computed-empty", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - - Key: "availability_zone", - Value: "", - Ok: false, - }, - - { - Name: "string-optional-computed-nil-diff", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "availability_zone", - Value: "", - Ok: false, - }, - - /* - * Lists - */ - - { - Name: "list-optional", - Schema: map[string]*Schema{ - "ports": { - Type: TypeList, - Optional: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports", - Value: []interface{}{}, - Ok: false, - }, - - /* - * Map - */ - - { - Name: "map-optional", - Schema: map[string]*Schema{ - "ports": { - Type: TypeMap, - Optional: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports", - Value: map[string]interface{}{}, - Ok: false, - }, - - /* - * Set - */ - - { - Name: "set-optional", - Schema: map[string]*Schema{ - "ports": { - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { return a.(int) }, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports", - Value: []interface{}{}, - Ok: false, - }, - - { - Name: "set-optional-key", - Schema: map[string]*Schema{ - "ports": { - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { return a.(int) }, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports.0", - Value: 0, - Ok: false, - }, - - { - Name: "bool-literal-empty", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "", - }, - }, - }, - - Key: "availability_zone", - Value: false, - Ok: true, - }, - - { - Name: "bool-literal-set", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - New: "true", - }, - }, - }, - - Key: "availability_zone", - Value: true, - Ok: true, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - d, err := schemaMap(tc.Schema).Data(tc.State, tc.Diff) - if err != nil { - t.Fatalf("%s err: %s", tc.Name, err) - } - - v, ok := d.GetOkExists(tc.Key) - if s, ok := v.(*Set); ok { - v = s.List() - } - - if !reflect.DeepEqual(v, tc.Value) { - t.Fatalf("Bad %s: \n%#v", tc.Name, v) - } - if ok != tc.Ok { - t.Fatalf("%s: expected ok: %t, got: %t", tc.Name, tc.Ok, ok) - } - }) - } -} - -func TestResourceDataTimeout(t *testing.T) { - cases := []struct { - Name string - Rd *ResourceData - Expected *ResourceTimeout - }{ - { - Name: "Basic example default", - Rd: &ResourceData{timeouts: timeoutForValues(10, 3, 0, 15, 0)}, - Expected: expectedTimeoutForValues(10, 3, 0, 15, 0), - }, - { - Name: "Resource and config match update, create", - Rd: &ResourceData{timeouts: timeoutForValues(10, 0, 3, 0, 0)}, - Expected: expectedTimeoutForValues(10, 0, 3, 0, 0), - }, - { - Name: "Resource provides default", - Rd: &ResourceData{timeouts: timeoutForValues(10, 0, 0, 0, 7)}, - Expected: expectedTimeoutForValues(10, 7, 7, 7, 7), - }, - { - Name: "Resource provides default and delete", - Rd: &ResourceData{timeouts: timeoutForValues(10, 0, 0, 15, 7)}, - Expected: expectedTimeoutForValues(10, 7, 7, 15, 7), - }, - { - Name: "Resource provides default, config overwrites other values", - Rd: &ResourceData{timeouts: timeoutForValues(10, 3, 0, 0, 13)}, - Expected: expectedTimeoutForValues(10, 3, 13, 13, 13), - }, - { - Name: "Resource has no config", - Rd: &ResourceData{}, - Expected: expectedTimeoutForValues(0, 0, 0, 0, 0), - }, - } - - keys := timeoutKeys() - for i, c := range cases { - t.Run(fmt.Sprintf("%d-%s", i, c.Name), func(t *testing.T) { - - for _, k := range keys { - got := c.Rd.Timeout(k) - var ex *time.Duration - switch k { - case TimeoutCreate: - ex = c.Expected.Create - case TimeoutRead: - ex = c.Expected.Read - case TimeoutUpdate: - ex = c.Expected.Update - case TimeoutDelete: - ex = c.Expected.Delete - case TimeoutDefault: - ex = c.Expected.Default - } - - if got > 0 && ex == nil { - t.Fatalf("Unexpected value in (%s), case %d check 1:\n\texpected: %#v\n\tgot: %#v", k, i, ex, got) - } - if got == 0 && ex != nil { - t.Fatalf("Unexpected value in (%s), case %d check 2:\n\texpected: %#v\n\tgot: %#v", k, i, *ex, got) - } - - // confirm values - if ex != nil { - if got != *ex { - t.Fatalf("Timeout %s case (%d) expected (%s), got (%s)", k, i, *ex, got) - } - } - } - - }) - } -} - -func TestResourceDataHasChange(t *testing.T) { - cases := []struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - Change bool - }{ - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Key: "availability_zone", - - Change: true, - }, - - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Key: "availability_zone", - - Change: false, - }, - - { - Schema: map[string]*Schema{ - "tags": &Schema{ - Type: TypeMap, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "tags.Name": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "foo", - }, - }, - }, - - Key: "tags", - - Change: true, - }, - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { return a.(int) }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "1", - "ports.80": "80", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "0", - }, - }, - }, - - Key: "ports", - - Change: true, - }, - - // https://github.com/hashicorp/terraform/issues/927 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { return a.(int) }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "1", - "ports.80": "80", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "tags.foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - - Key: "ports", - - Change: false, - }, - } - - for i, tc := range cases { - d, err := schemaMap(tc.Schema).Data(tc.State, tc.Diff) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := d.HasChange(tc.Key) - if actual != tc.Change { - t.Fatalf("Bad: %d %#v", i, actual) - } - } -} - -func TestResourceDataSet(t *testing.T) { - var testNilPtr *string - - cases := []struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - Value interface{} - Err bool - GetKey string - GetValue interface{} - - // GetPreProcess can be set to munge the return value before being - // compared to GetValue - GetPreProcess func(interface{}) interface{} - }{ - // #0: Basic good - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "availability_zone", - Value: "foo", - - GetKey: "availability_zone", - GetValue: "foo", - }, - - // #1: Basic int - { - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeInt, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "port", - Value: 80, - - GetKey: "port", - GetValue: 80, - }, - - // #2: Basic bool - { - Schema: map[string]*Schema{ - "vpc": &Schema{ - Type: TypeBool, - Optional: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "vpc", - Value: true, - - GetKey: "vpc", - GetValue: true, - }, - - // #3 - { - Schema: map[string]*Schema{ - "vpc": &Schema{ - Type: TypeBool, - Optional: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "vpc", - Value: false, - - GetKey: "vpc", - GetValue: false, - }, - - // #4: Invalid type - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "availability_zone", - Value: 80, - Err: true, - - GetKey: "availability_zone", - GetValue: "", - }, - - // #5: List of primitives, set list - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Computed: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports", - Value: []int{1, 2, 5}, - - GetKey: "ports", - GetValue: []interface{}{1, 2, 5}, - }, - - // #6: List of primitives, set list with error - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Computed: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ports", - Value: []interface{}{1, "NOPE", 5}, - Err: true, - - GetKey: "ports", - GetValue: []interface{}{}, - }, - - // #7: Set a list of maps - { - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{ - Type: TypeMap, - }, - }, - }, - - State: nil, - - Diff: nil, - - Key: "config_vars", - Value: []interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - map[string]interface{}{ - "bar": "baz", - }, - }, - Err: false, - - GetKey: "config_vars", - GetValue: []interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - map[string]interface{}{ - "bar": "baz", - }, - }, - }, - - // #8: Set, with list - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.0": "100", - "ports.1": "80", - "ports.2": "80", - }, - }, - - Key: "ports", - Value: []interface{}{100, 125, 125}, - - GetKey: "ports", - GetValue: []interface{}{100, 125}, - }, - - // #9: Set, with Set - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.100": "100", - "ports.80": "80", - "ports.81": "81", - }, - }, - - Key: "ports", - Value: &Set{ - m: map[string]interface{}{ - "1": 1, - "2": 2, - }, - }, - - GetKey: "ports", - GetValue: []interface{}{1, 2}, - }, - - // #10: Set single item - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "2", - "ports.100": "100", - "ports.80": "80", - }, - }, - - Key: "ports.100", - Value: 256, - Err: true, - - GetKey: "ports", - GetValue: []interface{}{100, 80}, - }, - - // #11: Set with nested set - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Elem: &Resource{ - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeInt, - }, - - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - }, - Set: func(a interface{}) int { - return a.(map[string]interface{})["port"].(int) - }, - }, - }, - - State: nil, - - Key: "ports", - Value: []interface{}{ - map[string]interface{}{ - "port": 80, - }, - }, - - GetKey: "ports", - GetValue: []interface{}{ - map[string]interface{}{ - "port": 80, - "set": []interface{}{}, - }, - }, - - GetPreProcess: func(v interface{}) interface{} { - if v == nil { - return v - } - s, ok := v.([]interface{}) - if !ok { - return v - } - for _, v := range s { - m, ok := v.(map[string]interface{}) - if !ok { - continue - } - if m["set"] == nil { - continue - } - if s, ok := m["set"].(*Set); ok { - m["set"] = s.List() - } - } - - return v - }, - }, - - // #12: List of floats, set list - { - Schema: map[string]*Schema{ - "ratios": &Schema{ - Type: TypeList, - Computed: true, - Elem: &Schema{Type: TypeFloat}, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ratios", - Value: []float64{1.0, 2.2, 5.5}, - - GetKey: "ratios", - GetValue: []interface{}{1.0, 2.2, 5.5}, - }, - - // #12: Set of floats, set list - { - Schema: map[string]*Schema{ - "ratios": &Schema{ - Type: TypeSet, - Computed: true, - Elem: &Schema{Type: TypeFloat}, - Set: func(a interface{}) int { - return int(math.Float64bits(a.(float64))) - }, - }, - }, - - State: nil, - - Diff: nil, - - Key: "ratios", - Value: []float64{1.0, 2.2, 5.5}, - - GetKey: "ratios", - GetValue: []interface{}{1.0, 2.2, 5.5}, - }, - - // #13: Basic pointer - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "availability_zone", - Value: testPtrTo("foo"), - - GetKey: "availability_zone", - GetValue: "foo", - }, - - // #14: Basic nil value - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "availability_zone", - Value: testPtrTo(nil), - - GetKey: "availability_zone", - GetValue: "", - }, - - // #15: Basic nil pointer - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: nil, - - Key: "availability_zone", - Value: testNilPtr, - - GetKey: "availability_zone", - GetValue: "", - }, - - // #16: Set in a list - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "set": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - }, - }, - }, - - State: nil, - - Key: "ports", - Value: []interface{}{ - map[string]interface{}{ - "set": []interface{}{ - 1, - }, - }, - }, - - GetKey: "ports", - GetValue: []interface{}{ - map[string]interface{}{ - "set": []interface{}{ - 1, - }, - }, - }, - GetPreProcess: func(v interface{}) interface{} { - if v == nil { - return v - } - s, ok := v.([]interface{}) - if !ok { - return v - } - for _, v := range s { - m, ok := v.(map[string]interface{}) - if !ok { - continue - } - if m["set"] == nil { - continue - } - if s, ok := m["set"].(*Set); ok { - m["set"] = s.List() - } - } - - return v - }, - }, - } - - oldEnv := os.Getenv(PanicOnErr) - os.Setenv(PanicOnErr, "") - defer os.Setenv(PanicOnErr, oldEnv) - - for i, tc := range cases { - d, err := schemaMap(tc.Schema).Data(tc.State, tc.Diff) - if err != nil { - t.Fatalf("err: %s", err) - } - - err = d.Set(tc.Key, tc.Value) - if err != nil != tc.Err { - t.Fatalf("%d err: %s", i, err) - } - - v := d.Get(tc.GetKey) - if s, ok := v.(*Set); ok { - v = s.List() - } - - if tc.GetPreProcess != nil { - v = tc.GetPreProcess(v) - } - - if !reflect.DeepEqual(v, tc.GetValue) { - t.Fatalf("Get Bad: %d\n\n%#v", i, v) - } - } -} - -func TestResourceDataState_dynamicAttributes(t *testing.T) { - cases := []struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Set map[string]interface{} - UnsafeSet map[string]string - Result *terraform.InstanceState - }{ - { - Schema: map[string]*Schema{ - "__has_dynamic_attributes": { - Type: TypeString, - Optional: true, - }, - - "schema_field": { - Type: TypeString, - Required: true, - }, - }, - - State: nil, - - Diff: nil, - - Set: map[string]interface{}{ - "schema_field": "present", - }, - - UnsafeSet: map[string]string{ - "test1": "value", - "test2": "value", - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "schema_field": "present", - "test1": "value", - "test2": "value", - }, - }, - }, - } - - for i, tc := range cases { - d, err := schemaMap(tc.Schema).Data(tc.State, tc.Diff) - if err != nil { - t.Fatalf("err: %s", err) - } - - for k, v := range tc.Set { - d.Set(k, v) - } - - for k, v := range tc.UnsafeSet { - d.UnsafeSetFieldRaw(k, v) - } - - // Set an ID so that the state returned is not nil - idSet := false - if d.Id() == "" { - idSet = true - d.SetId("foo") - } - - actual := d.State() - - // If we set an ID, then undo what we did so the comparison works - if actual != nil && idSet { - actual.ID = "" - delete(actual.Attributes, "id") - } - - if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("Bad: %d\n\n%#v\n\nExpected:\n\n%#v", i, actual, tc.Result) - } - } -} - -func TestResourceDataState_schema(t *testing.T) { - cases := []struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Set map[string]interface{} - Result *terraform.InstanceState - Partial []string - }{ - // #0 Basic primitive in diff - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - }, - - // #1 Basic primitive set override - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Set: map[string]interface{}{ - "availability_zone": "bar", - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "bar", - }, - }, - }, - - // #2 - { - Schema: map[string]*Schema{ - "vpc": &Schema{ - Type: TypeBool, - Optional: true, - }, - }, - - State: nil, - - Diff: nil, - - Set: map[string]interface{}{ - "vpc": true, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "vpc": "true", - }, - }, - }, - - // #3 Basic primitive with StateFunc set - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - StateFunc: func(interface{}) string { return "" }, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - NewExtra: "foo!", - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - }, - - // #4 List - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "1", - "ports.0": "80", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "2", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "100", - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "2", - "ports.0": "80", - "ports.1": "100", - }, - }, - }, - - // #5 List of resources - { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ingress.#": "1", - "ingress.0.from": "80", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ingress.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "2", - }, - "ingress.0.from": &terraform.ResourceAttrDiff{ - Old: "80", - New: "150", - }, - "ingress.1.from": &terraform.ResourceAttrDiff{ - Old: "", - New: "100", - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ingress.#": "2", - "ingress.0.from": "150", - "ingress.1.from": "100", - }, - }, - }, - - // #6 List of maps - { - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{ - Type: TypeMap, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.#": "2", - "config_vars.0.%": "2", - "config_vars.0.foo": "bar", - "config_vars.0.bar": "bar", - "config_vars.1.%": "1", - "config_vars.1.bar": "baz", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.0.bar": &terraform.ResourceAttrDiff{ - NewRemoved: true, - }, - }, - }, - - Set: map[string]interface{}{ - "config_vars": []map[string]interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - map[string]interface{}{ - "baz": "bang", - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.#": "2", - "config_vars.0.%": "1", - "config_vars.0.foo": "bar", - "config_vars.1.%": "1", - "config_vars.1.baz": "bang", - }, - }, - }, - - // #7 List of maps with removal in diff - { - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{ - Type: TypeMap, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.#": "1", - "config_vars.0.FOO": "bar", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "0", - }, - "config_vars.0.FOO": &terraform.ResourceAttrDiff{ - Old: "bar", - NewRemoved: true, - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.#": "0", - }, - }, - }, - - // #8 Basic state with other keys - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Result: &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "availability_zone": "foo", - }, - }, - }, - - // #9 Sets - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.100": "100", - "ports.80": "80", - "ports.81": "81", - }, - }, - - Diff: nil, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.80": "80", - "ports.81": "81", - "ports.100": "100", - }, - }, - }, - - // #10 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Diff: nil, - - Set: map[string]interface{}{ - "ports": []interface{}{100, 80}, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "2", - "ports.80": "80", - "ports.100": "100", - }, - }, - }, - - // #11 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "order": &Schema{ - Type: TypeInt, - }, - - "a": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - - "b": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - }, - }, - }, - Set: func(a interface{}) int { - m := a.(map[string]interface{}) - return m["order"].(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "2", - "ports.10.order": "10", - "ports.10.a.#": "1", - "ports.10.a.0": "80", - "ports.20.order": "20", - "ports.20.b.#": "1", - "ports.20.b.0": "100", - }, - }, - - Set: map[string]interface{}{ - "ports": []interface{}{ - map[string]interface{}{ - "order": 20, - "b": []interface{}{100}, - }, - map[string]interface{}{ - "order": 10, - "a": []interface{}{80}, - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "2", - "ports.10.order": "10", - "ports.10.a.#": "1", - "ports.10.a.0": "80", - "ports.10.b.#": "0", - "ports.20.order": "20", - "ports.20.a.#": "0", - "ports.20.b.#": "1", - "ports.20.b.0": "100", - }, - }, - }, - - /* - * PARTIAL STATES - */ - - // #12 Basic primitive - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Partial: []string{}, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - - // #13 List - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "1", - "ports.0": "80", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "2", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "100", - }, - }, - }, - - Partial: []string{}, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "1", - "ports.0": "80", - }, - }, - }, - - // #14 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Partial: []string{}, - - Set: map[string]interface{}{ - "ports": []interface{}{}, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - - // #15 List of resources - { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ingress.#": "1", - "ingress.0.from": "80", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ingress.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "2", - }, - "ingress.0.from": &terraform.ResourceAttrDiff{ - Old: "80", - New: "150", - }, - "ingress.1.from": &terraform.ResourceAttrDiff{ - Old: "", - New: "100", - }, - }, - }, - - Partial: []string{}, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ingress.#": "1", - "ingress.0.from": "80", - }, - }, - }, - - // #16 List of maps - { - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{ - Type: TypeMap, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.#": "2", - "config_vars.0.foo": "bar", - "config_vars.0.bar": "bar", - "config_vars.1.bar": "baz", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.0.bar": &terraform.ResourceAttrDiff{ - NewRemoved: true, - }, - }, - }, - - Set: map[string]interface{}{ - "config_vars": []map[string]interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - map[string]interface{}{ - "baz": "bang", - }, - }, - }, - - Partial: []string{}, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - // TODO: broken, shouldn't bar be removed? - "config_vars.#": "2", - "config_vars.0.%": "2", - "config_vars.0.foo": "bar", - "config_vars.0.bar": "bar", - "config_vars.1.%": "1", - "config_vars.1.bar": "baz", - }, - }, - }, - - // #17 Sets - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.100": "100", - "ports.80": "80", - "ports.81": "81", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.120": &terraform.ResourceAttrDiff{ - New: "120", - }, - }, - }, - - Partial: []string{}, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.80": "80", - "ports.81": "81", - "ports.100": "100", - }, - }, - }, - - // #18 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Partial: []string{}, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - - // #19 Maps - { - Schema: map[string]*Schema{ - "tags": &Schema{ - Type: TypeMap, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "tags.Name": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "tags.%": "1", - "tags.Name": "foo", - }, - }, - }, - - // #20 empty computed map - { - Schema: map[string]*Schema{ - "tags": &Schema{ - Type: TypeMap, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "tags.Name": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - - Set: map[string]interface{}{ - "tags": map[string]string{}, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "tags.%": "0", - }, - }, - }, - - // #21 - { - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{}, - }, - }, - - // #22 - { - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Set: map[string]interface{}{ - "foo": "bar", - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - }, - - // #23 Set of maps - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "uuids": &Schema{Type: TypeMap}, - }, - }, - Set: func(a interface{}) int { - m := a.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.10.uuids.#": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Set: map[string]interface{}{ - "ports": []interface{}{ - map[string]interface{}{ - "index": 10, - "uuids": map[string]interface{}{ - "80": "value", - }, - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "1", - "ports.10.index": "10", - "ports.10.uuids.%": "1", - "ports.10.uuids.80": "value", - }, - }, - }, - - // #24 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.100": "100", - "ports.80": "80", - "ports.81": "81", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "3", - New: "0", - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "0", - }, - }, - }, - - // #25 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Diff: nil, - - Set: map[string]interface{}{ - "ports": []interface{}{}, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "0", - }, - }, - }, - - // #26 - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Diff: nil, - - Set: map[string]interface{}{ - "ports": []interface{}{}, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "0", - }, - }, - }, - - // #27 Set lists - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{Type: TypeInt}, - "uuids": &Schema{Type: TypeMap}, - }, - }, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Set: map[string]interface{}{ - "ports": []interface{}{ - map[string]interface{}{ - "index": 10, - "uuids": map[string]interface{}{ - "80": "value", - }, - }, - }, - }, - - Result: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "1", - "ports.0.index": "10", - "ports.0.uuids.%": "1", - "ports.0.uuids.80": "value", - }, - }, - }, - } - - for i, tc := range cases { - d, err := schemaMap(tc.Schema).Data(tc.State, tc.Diff) - if err != nil { - t.Fatalf("err: %s", err) - } - - for k, v := range tc.Set { - if err := d.Set(k, v); err != nil { - t.Fatalf("%d err: %s", i, err) - } - } - - // Set an ID so that the state returned is not nil - idSet := false - if d.Id() == "" { - idSet = true - d.SetId("foo") - } - - // If we have partial, then enable partial state mode. - if tc.Partial != nil { - d.Partial(true) - for _, k := range tc.Partial { - d.SetPartial(k) - } - } - - actual := d.State() - - // If we set an ID, then undo what we did so the comparison works - if actual != nil && idSet { - actual.ID = "" - delete(actual.Attributes, "id") - } - - if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("Bad: %d\n\n%#v\n\nExpected:\n\n%#v", i, actual, tc.Result) - } - } -} - -func TestResourceData_nonStringValuesInMap(t *testing.T) { - cases := []struct { - Schema map[string]*Schema - Diff *terraform.InstanceDiff - MapFieldName string - ItemName string - ExpectedType string - }{ - { - Schema: map[string]*Schema{ - "boolMap": &Schema{ - Type: TypeMap, - Elem: TypeBool, - Optional: true, - }, - }, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "boolMap.%": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "boolMap.boolField": &terraform.ResourceAttrDiff{ - Old: "", - New: "true", - }, - }, - }, - MapFieldName: "boolMap", - ItemName: "boolField", - ExpectedType: "bool", - }, - { - Schema: map[string]*Schema{ - "intMap": &Schema{ - Type: TypeMap, - Elem: TypeInt, - Optional: true, - }, - }, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "intMap.%": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "intMap.intField": &terraform.ResourceAttrDiff{ - Old: "", - New: "8", - }, - }, - }, - MapFieldName: "intMap", - ItemName: "intField", - ExpectedType: "int", - }, - { - Schema: map[string]*Schema{ - "floatMap": &Schema{ - Type: TypeMap, - Elem: TypeFloat, - Optional: true, - }, - }, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "floatMap.%": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "floatMap.floatField": &terraform.ResourceAttrDiff{ - Old: "", - New: "8.22", - }, - }, - }, - MapFieldName: "floatMap", - ItemName: "floatField", - ExpectedType: "float64", - }, - } - - for _, c := range cases { - d, err := schemaMap(c.Schema).Data(nil, c.Diff) - if err != nil { - t.Fatalf("err: %s", err) - } - - m, ok := d.Get(c.MapFieldName).(map[string]interface{}) - if !ok { - t.Fatalf("expected %q to be castable to a map", c.MapFieldName) - } - field, ok := m[c.ItemName] - if !ok { - t.Fatalf("expected %q in the map", c.ItemName) - } - - typeName := reflect.TypeOf(field).Name() - if typeName != c.ExpectedType { - t.Fatalf("expected %q to be %q, it is %q.", - c.ItemName, c.ExpectedType, typeName) - } - } -} - -func TestResourceDataSetConnInfo(t *testing.T) { - d := &ResourceData{} - d.SetId("foo") - d.SetConnInfo(map[string]string{ - "foo": "bar", - }) - - expected := map[string]string{ - "foo": "bar", - } - - actual := d.State() - if !reflect.DeepEqual(actual.Ephemeral.ConnInfo, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceDataSetMeta_Timeouts(t *testing.T) { - d := &ResourceData{} - d.SetId("foo") - - rt := ResourceTimeout{ - Create: DefaultTimeout(7 * time.Minute), - } - - d.timeouts = &rt - - expected := expectedForValues(7, 0, 0, 0, 0) - - actual := d.State() - if !reflect.DeepEqual(actual.Meta[TimeoutKey], expected) { - t.Fatalf("Bad Meta_timeout match:\n\texpected: %#v\n\tgot: %#v", expected, actual.Meta[TimeoutKey]) - } -} - -func TestResourceDataSetId(t *testing.T) { - d := &ResourceData{ - state: &terraform.InstanceState{ - ID: "test", - Attributes: map[string]string{ - "id": "test", - }, - }, - } - d.SetId("foo") - - actual := d.State() - - // SetId should set both the ID field as well as the attribute, to aid in - // transitioning to the new type system. - if actual.ID != "foo" || actual.Attributes["id"] != "foo" { - t.Fatalf("bad: %#v", actual) - } - - d.SetId("") - actual = d.State() - if actual != nil { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceDataSetId_clear(t *testing.T) { - d := &ResourceData{ - state: &terraform.InstanceState{ID: "bar"}, - } - d.SetId("") - - actual := d.State() - if actual != nil { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceDataSetId_override(t *testing.T) { - d := &ResourceData{ - state: &terraform.InstanceState{ID: "bar"}, - } - d.SetId("foo") - - actual := d.State() - if actual.ID != "foo" { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceDataSetType(t *testing.T) { - d := &ResourceData{} - d.SetId("foo") - d.SetType("bar") - - actual := d.State() - if v := actual.Ephemeral.Type; v != "bar" { - t.Fatalf("bad: %#v", actual) - } -} - -func testPtrTo(raw interface{}) interface{} { - return &raw -} diff --git a/helper/schema/resource_diff.go b/helper/schema/resource_diff.go deleted file mode 100644 index 47b548104..000000000 --- a/helper/schema/resource_diff.go +++ /dev/null @@ -1,559 +0,0 @@ -package schema - -import ( - "errors" - "fmt" - "reflect" - "strings" - "sync" - - "github.com/hashicorp/terraform/terraform" -) - -// newValueWriter is a minor re-implementation of MapFieldWriter to include -// keys that should be marked as computed, to represent the new part of a -// pseudo-diff. -type newValueWriter struct { - *MapFieldWriter - - // A list of keys that should be marked as computed. - computedKeys map[string]bool - - // A lock to prevent races on writes. The underlying writer will have one as - // well - this is for computed keys. - lock sync.Mutex - - // To be used with init. - once sync.Once -} - -// init performs any initialization tasks for the newValueWriter. -func (w *newValueWriter) init() { - if w.computedKeys == nil { - w.computedKeys = make(map[string]bool) - } -} - -// WriteField overrides MapValueWriter's WriteField, adding the ability to flag -// the address as computed. -func (w *newValueWriter) WriteField(address []string, value interface{}, computed bool) error { - // Fail the write if we have a non-nil value and computed is true. - // NewComputed values should not have a value when written. - if value != nil && computed { - return errors.New("Non-nil value with computed set") - } - - if err := w.MapFieldWriter.WriteField(address, value); err != nil { - return err - } - - w.once.Do(w.init) - - w.lock.Lock() - defer w.lock.Unlock() - if computed { - w.computedKeys[strings.Join(address, ".")] = true - } - return nil -} - -// ComputedKeysMap returns the underlying computed keys map. -func (w *newValueWriter) ComputedKeysMap() map[string]bool { - w.once.Do(w.init) - return w.computedKeys -} - -// newValueReader is a minor re-implementation of MapFieldReader and is the -// read counterpart to MapValueWriter, allowing the read of keys flagged as -// computed to accommodate the diff override logic in ResourceDiff. -type newValueReader struct { - *MapFieldReader - - // The list of computed keys from a newValueWriter. - computedKeys map[string]bool -} - -// ReadField reads the values from the underlying writer, returning the -// computed value if it is found as well. -func (r *newValueReader) ReadField(address []string) (FieldReadResult, error) { - addrKey := strings.Join(address, ".") - v, err := r.MapFieldReader.ReadField(address) - if err != nil { - return FieldReadResult{}, err - } - for computedKey := range r.computedKeys { - if childAddrOf(addrKey, computedKey) { - if strings.HasSuffix(addrKey, ".#") { - // This is a count value for a list or set that has been marked as - // computed, or a sub-list/sub-set of a complex resource that has - // been marked as computed. We need to pass through to other readers - // so that an accurate previous count can be fetched for the diff. - v.Exists = false - } - v.Computed = true - } - } - - return v, nil -} - -// ResourceDiff is used to query and make custom changes to an in-flight diff. -// It can be used to veto particular changes in the diff, customize the diff -// that has been created, or diff values not controlled by config. -// -// The object functions similar to ResourceData, however most notably lacks -// Set, SetPartial, and Partial, as it should be used to change diff values -// only. Most other first-class ResourceData functions exist, namely Get, -// GetOk, HasChange, and GetChange exist. -// -// All functions in ResourceDiff, save for ForceNew, can only be used on -// computed fields. -type ResourceDiff struct { - // The schema for the resource being worked on. - schema map[string]*Schema - - // The current config for this resource. - config *terraform.ResourceConfig - - // The state for this resource as it exists post-refresh, after the initial - // diff. - state *terraform.InstanceState - - // The diff created by Terraform. This diff is used, along with state, - // config, and custom-set diff data, to provide a multi-level reader - // experience similar to ResourceData. - diff *terraform.InstanceDiff - - // The internal reader structure that contains the state, config, the default - // diff, and the new diff. - multiReader *MultiLevelFieldReader - - // A writer that writes overridden new fields. - newWriter *newValueWriter - - // Tracks which keys have been updated by ResourceDiff to ensure that the - // diff does not get re-run on keys that were not touched, or diffs that were - // just removed (re-running on the latter would just roll back the removal). - updatedKeys map[string]bool - - // Tracks which keys were flagged as forceNew. These keys are not saved in - // newWriter, but we need to track them so that they can be re-diffed later. - forcedNewKeys map[string]bool -} - -// newResourceDiff creates a new ResourceDiff instance. -func newResourceDiff(schema map[string]*Schema, config *terraform.ResourceConfig, state *terraform.InstanceState, diff *terraform.InstanceDiff) *ResourceDiff { - d := &ResourceDiff{ - config: config, - state: state, - diff: diff, - schema: schema, - } - - d.newWriter = &newValueWriter{ - MapFieldWriter: &MapFieldWriter{Schema: d.schema}, - } - readers := make(map[string]FieldReader) - var stateAttributes map[string]string - if d.state != nil { - stateAttributes = d.state.Attributes - readers["state"] = &MapFieldReader{ - Schema: d.schema, - Map: BasicMapReader(stateAttributes), - } - } - if d.config != nil { - readers["config"] = &ConfigFieldReader{ - Schema: d.schema, - Config: d.config, - } - } - if d.diff != nil { - readers["diff"] = &DiffFieldReader{ - Schema: d.schema, - Diff: d.diff, - Source: &MultiLevelFieldReader{ - Levels: []string{"state", "config"}, - Readers: readers, - }, - } - } - readers["newDiff"] = &newValueReader{ - MapFieldReader: &MapFieldReader{ - Schema: d.schema, - Map: BasicMapReader(d.newWriter.Map()), - }, - computedKeys: d.newWriter.ComputedKeysMap(), - } - d.multiReader = &MultiLevelFieldReader{ - Levels: []string{ - "state", - "config", - "diff", - "newDiff", - }, - - Readers: readers, - } - - d.updatedKeys = make(map[string]bool) - d.forcedNewKeys = make(map[string]bool) - - return d -} - -// UpdatedKeys returns the keys that were updated by this ResourceDiff run. -// These are the only keys that a diff should be re-calculated for. -// -// This is the combined result of both keys for which diff values were updated -// for or cleared, and also keys that were flagged to be re-diffed as a result -// of ForceNew. -func (d *ResourceDiff) UpdatedKeys() []string { - var s []string - for k := range d.updatedKeys { - s = append(s, k) - } - for k := range d.forcedNewKeys { - for _, l := range s { - if k == l { - break - } - } - s = append(s, k) - } - return s -} - -// Clear wipes the diff for a particular key. It is called by ResourceDiff's -// functionality to remove any possibility of conflicts, but can be called on -// its own to just remove a specific key from the diff completely. -// -// Note that this does not wipe an override. This function is only allowed on -// computed keys. -func (d *ResourceDiff) Clear(key string) error { - if err := d.checkKey(key, "Clear", true); err != nil { - return err - } - - return d.clear(key) -} - -func (d *ResourceDiff) clear(key string) error { - // Check the schema to make sure that this key exists first. - schemaL := addrToSchema(strings.Split(key, "."), d.schema) - if len(schemaL) == 0 { - return fmt.Errorf("%s is not a valid key", key) - } - - for k := range d.diff.Attributes { - if strings.HasPrefix(k, key) { - delete(d.diff.Attributes, k) - } - } - return nil -} - -// GetChangedKeysPrefix helps to implement Resource.CustomizeDiff -// where we need to act on all nested fields -// without calling out each one separately -func (d *ResourceDiff) GetChangedKeysPrefix(prefix string) []string { - keys := make([]string, 0) - for k := range d.diff.Attributes { - if strings.HasPrefix(k, prefix) { - keys = append(keys, k) - } - } - return keys -} - -// diffChange helps to implement resourceDiffer and derives its change values -// from ResourceDiff's own change data, in addition to existing diff, config, and state. -func (d *ResourceDiff) diffChange(key string) (interface{}, interface{}, bool, bool, bool) { - old, new, customized := d.getChange(key) - - if !old.Exists { - old.Value = nil - } - if !new.Exists || d.removed(key) { - new.Value = nil - } - - return old.Value, new.Value, !reflect.DeepEqual(old.Value, new.Value), new.Computed, customized -} - -// SetNew is used to set a new diff value for the mentioned key. The value must -// be correct for the attribute's schema (mostly relevant for maps, lists, and -// sets). The original value from the state is used as the old value. -// -// This function is only allowed on computed attributes. -func (d *ResourceDiff) SetNew(key string, value interface{}) error { - if err := d.checkKey(key, "SetNew", false); err != nil { - return err - } - - return d.setDiff(key, value, false) -} - -// SetNewComputed functions like SetNew, except that it blanks out a new value -// and marks it as computed. -// -// This function is only allowed on computed attributes. -func (d *ResourceDiff) SetNewComputed(key string) error { - if err := d.checkKey(key, "SetNewComputed", false); err != nil { - return err - } - - return d.setDiff(key, nil, true) -} - -// setDiff performs common diff setting behaviour. -func (d *ResourceDiff) setDiff(key string, new interface{}, computed bool) error { - if err := d.clear(key); err != nil { - return err - } - - if err := d.newWriter.WriteField(strings.Split(key, "."), new, computed); err != nil { - return fmt.Errorf("Cannot set new diff value for key %s: %s", key, err) - } - - d.updatedKeys[key] = true - - return nil -} - -// ForceNew force-flags ForceNew in the schema for a specific key, and -// re-calculates its diff, effectively causing this attribute to force a new -// resource. -// -// Keep in mind that forcing a new resource will force a second run of the -// resource's CustomizeDiff function (with a new ResourceDiff) once the current -// one has completed. This second run is performed without state. This behavior -// will be the same as if a new resource is being created and is performed to -// ensure that the diff looks like the diff for a new resource as much as -// possible. CustomizeDiff should expect such a scenario and act correctly. -// -// This function is a no-op/error if there is no diff. -// -// Note that the change to schema is permanent for the lifecycle of this -// specific ResourceDiff instance. -func (d *ResourceDiff) ForceNew(key string) error { - if !d.HasChange(key) { - return fmt.Errorf("ForceNew: No changes for %s", key) - } - - keyParts := strings.Split(key, ".") - var schema *Schema - schemaL := addrToSchema(keyParts, d.schema) - if len(schemaL) > 0 { - schema = schemaL[len(schemaL)-1] - } else { - return fmt.Errorf("ForceNew: %s is not a valid key", key) - } - - schema.ForceNew = true - - // Flag this for a re-diff. Don't save any values to guarantee that existing - // diffs aren't messed with, as this gets messy when dealing with complex - // structures, zero values, etc. - d.forcedNewKeys[keyParts[0]] = true - - return nil -} - -// Get hands off to ResourceData.Get. -func (d *ResourceDiff) Get(key string) interface{} { - r, _ := d.GetOk(key) - return r -} - -// GetChange gets the change between the state and diff, checking first to see -// if an overridden diff exists. -// -// This implementation differs from ResourceData's in the way that we first get -// results from the exact levels for the new diff, then from state and diff as -// per normal. -func (d *ResourceDiff) GetChange(key string) (interface{}, interface{}) { - old, new, _ := d.getChange(key) - return old.Value, new.Value -} - -// GetOk functions the same way as ResourceData.GetOk, but it also checks the -// new diff levels to provide data consistent with the current state of the -// customized diff. -func (d *ResourceDiff) GetOk(key string) (interface{}, bool) { - r := d.get(strings.Split(key, "."), "newDiff") - exists := r.Exists && !r.Computed - if exists { - // If it exists, we also want to verify it is not the zero-value. - value := r.Value - zero := r.Schema.Type.Zero() - - if eq, ok := value.(Equal); ok { - exists = !eq.Equal(zero) - } else { - exists = !reflect.DeepEqual(value, zero) - } - } - - return r.Value, exists -} - -// GetOkExists functions the same way as GetOkExists within ResourceData, but -// it also checks the new diff levels to provide data consistent with the -// current state of the customized diff. -// -// This is nearly the same function as GetOk, yet it does not check -// for the zero value of the attribute's type. This allows for attributes -// without a default, to fully check for a literal assignment, regardless -// of the zero-value for that type. -func (d *ResourceDiff) GetOkExists(key string) (interface{}, bool) { - r := d.get(strings.Split(key, "."), "newDiff") - exists := r.Exists && !r.Computed - return r.Value, exists -} - -// NewValueKnown returns true if the new value for the given key is available -// as its final value at diff time. If the return value is false, this means -// either the value is based of interpolation that was unavailable at diff -// time, or that the value was explicitly marked as computed by SetNewComputed. -func (d *ResourceDiff) NewValueKnown(key string) bool { - r := d.get(strings.Split(key, "."), "newDiff") - return !r.Computed -} - -// HasChange checks to see if there is a change between state and the diff, or -// in the overridden diff. -func (d *ResourceDiff) HasChange(key string) bool { - old, new := d.GetChange(key) - - // If the type implements the Equal interface, then call that - // instead of just doing a reflect.DeepEqual. An example where this is - // needed is *Set - if eq, ok := old.(Equal); ok { - return !eq.Equal(new) - } - - return !reflect.DeepEqual(old, new) -} - -// Id returns the ID of this resource. -// -// Note that technically, ID does not change during diffs (it either has -// already changed in the refresh, or will change on update), hence we do not -// support updating the ID or fetching it from anything else other than state. -func (d *ResourceDiff) Id() string { - var result string - - if d.state != nil { - result = d.state.ID - } - return result -} - -// getChange gets values from two different levels, designed for use in -// diffChange, HasChange, and GetChange. -// -// This implementation differs from ResourceData's in the way that we first get -// results from the exact levels for the new diff, then from state and diff as -// per normal. -func (d *ResourceDiff) getChange(key string) (getResult, getResult, bool) { - old := d.get(strings.Split(key, "."), "state") - var new getResult - for p := range d.updatedKeys { - if childAddrOf(key, p) { - new = d.getExact(strings.Split(key, "."), "newDiff") - return old, new, true - } - } - new = d.get(strings.Split(key, "."), "newDiff") - return old, new, false -} - -// removed checks to see if the key is present in the existing, pre-customized -// diff and if it was marked as NewRemoved. -func (d *ResourceDiff) removed(k string) bool { - diff, ok := d.diff.Attributes[k] - if !ok { - return false - } - return diff.NewRemoved -} - -// get performs the appropriate multi-level reader logic for ResourceDiff, -// starting at source. Refer to newResourceDiff for the level order. -func (d *ResourceDiff) get(addr []string, source string) getResult { - result, err := d.multiReader.ReadFieldMerge(addr, source) - if err != nil { - panic(err) - } - - return d.finalizeResult(addr, result) -} - -// getExact gets an attribute from the exact level referenced by source. -func (d *ResourceDiff) getExact(addr []string, source string) getResult { - result, err := d.multiReader.ReadFieldExact(addr, source) - if err != nil { - panic(err) - } - - return d.finalizeResult(addr, result) -} - -// finalizeResult does some post-processing of the result produced by get and getExact. -func (d *ResourceDiff) finalizeResult(addr []string, result FieldReadResult) getResult { - // If the result doesn't exist, then we set the value to the zero value - var schema *Schema - if schemaL := addrToSchema(addr, d.schema); len(schemaL) > 0 { - schema = schemaL[len(schemaL)-1] - } - - if result.Value == nil && schema != nil { - result.Value = result.ValueOrZero(schema) - } - - // Transform the FieldReadResult into a getResult. It might be worth - // merging these two structures one day. - return getResult{ - Value: result.Value, - ValueProcessed: result.ValueProcessed, - Computed: result.Computed, - Exists: result.Exists, - Schema: schema, - } -} - -// childAddrOf does a comparison of two addresses to see if one is the child of -// the other. -func childAddrOf(child, parent string) bool { - cs := strings.Split(child, ".") - ps := strings.Split(parent, ".") - if len(ps) > len(cs) { - return false - } - return reflect.DeepEqual(ps, cs[:len(ps)]) -} - -// checkKey checks the key to make sure it exists and is computed. -func (d *ResourceDiff) checkKey(key, caller string, nested bool) error { - var schema *Schema - if nested { - keyParts := strings.Split(key, ".") - schemaL := addrToSchema(keyParts, d.schema) - if len(schemaL) > 0 { - schema = schemaL[len(schemaL)-1] - } - } else { - s, ok := d.schema[key] - if ok { - schema = s - } - } - if schema == nil { - return fmt.Errorf("%s: invalid key: %s", caller, key) - } - if !schema.Computed { - return fmt.Errorf("%s only operates on computed keys - %s is not one", caller, key) - } - return nil -} diff --git a/helper/schema/resource_diff_test.go b/helper/schema/resource_diff_test.go deleted file mode 100644 index e6897b731..000000000 --- a/helper/schema/resource_diff_test.go +++ /dev/null @@ -1,2045 +0,0 @@ -package schema - -import ( - "fmt" - "reflect" - "sort" - "testing" - - "github.com/davecgh/go-spew/spew" - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/terraform" -) - -// testSetFunc is a very simple function we use to test a foo/bar complex set. -// Both "foo" and "bar" are int values. -// -// This is not foolproof as since it performs sums, you can run into -// collisions. Spec tests accordingly. :P -func testSetFunc(v interface{}) int { - m := v.(map[string]interface{}) - return m["foo"].(int) + m["bar"].(int) -} - -// resourceDiffTestCase provides a test case struct for SetNew and SetDiff. -type resourceDiffTestCase struct { - Name string - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - OldValue interface{} - NewValue interface{} - Expected *terraform.InstanceDiff - ExpectedKeys []string - ExpectedError bool -} - -// testDiffCases produces a list of test cases for use with SetNew and SetDiff. -func testDiffCases(t *testing.T, oldPrefix string, oldOffset int, computed bool) []resourceDiffTestCase { - return []resourceDiffTestCase{ - resourceDiffTestCase{ - Name: "basic primitive diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "foo", - NewValue: "qux", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: func() string { - if computed { - return "" - } - return "qux" - }(), - NewComputed: computed, - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "basic set diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeString}, - Set: HashString, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo.#": "1", - "foo.1996459178": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": []interface{}{"baz"}, - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.1996459178": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "", - NewRemoved: true, - }, - "foo.2015626392": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - Key: "foo", - NewValue: []interface{}{"qux"}, - Expected: &terraform.InstanceDiff{ - Attributes: func() map[string]*terraform.ResourceAttrDiff { - result := map[string]*terraform.ResourceAttrDiff{} - if computed { - result["foo.#"] = &terraform.ResourceAttrDiff{ - Old: "1", - New: "", - NewComputed: true, - } - } else { - result["foo.2800005064"] = &terraform.ResourceAttrDiff{ - Old: "", - New: "qux", - } - result["foo.1996459178"] = &terraform.ResourceAttrDiff{ - Old: "bar", - New: "", - NewRemoved: true, - } - } - return result - }(), - }, - }, - resourceDiffTestCase{ - Name: "basic list diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeString}, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo.#": "1", - "foo.0": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": []interface{}{"baz"}, - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.0": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "foo", - NewValue: []interface{}{"qux"}, - Expected: &terraform.InstanceDiff{ - Attributes: func() map[string]*terraform.ResourceAttrDiff { - result := make(map[string]*terraform.ResourceAttrDiff) - if computed { - result["foo.#"] = &terraform.ResourceAttrDiff{ - Old: "1", - New: "", - NewComputed: true, - } - } else { - result["foo.0"] = &terraform.ResourceAttrDiff{ - Old: "bar", - New: "qux", - } - } - return result - }(), - }, - }, - resourceDiffTestCase{ - Name: "basic map diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeMap, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo.%": "1", - "foo.bar": "baz", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": map[string]interface{}{"bar": "qux"}, - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.bar": &terraform.ResourceAttrDiff{ - Old: "baz", - New: "qux", - }, - }, - }, - Key: "foo", - NewValue: map[string]interface{}{"bar": "quux"}, - Expected: &terraform.InstanceDiff{ - Attributes: func() map[string]*terraform.ResourceAttrDiff { - result := make(map[string]*terraform.ResourceAttrDiff) - if computed { - result["foo.%"] = &terraform.ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - } - result["foo.bar"] = &terraform.ResourceAttrDiff{ - Old: "baz", - New: "", - NewRemoved: true, - } - } else { - result["foo.bar"] = &terraform.ResourceAttrDiff{ - Old: "baz", - New: "quux", - } - } - return result - }(), - }, - }, - resourceDiffTestCase{ - Name: "additional diff with primitive", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - "one": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - "one": "two", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "one", - NewValue: "four", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - "one": &terraform.ResourceAttrDiff{ - Old: "two", - New: func() string { - if computed { - return "" - } - return "four" - }(), - NewComputed: computed, - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "additional diff with primitive computed only", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - "one": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - "one": "two", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "one", - NewValue: "three", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - "one": &terraform.ResourceAttrDiff{ - Old: "two", - New: func() string { - if computed { - return "" - } - return "three" - }(), - NewComputed: computed, - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "complex-ish set diff", - Schema: map[string]*Schema{ - "top": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - Computed: true, - }, - "bar": &Schema{ - Type: TypeInt, - Optional: true, - Computed: true, - }, - }, - }, - Set: testSetFunc, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "top.#": "2", - "top.3.foo": "1", - "top.3.bar": "2", - "top.23.foo": "11", - "top.23.bar": "12", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "top": []interface{}{ - map[string]interface{}{ - "foo": 1, - "bar": 3, - }, - map[string]interface{}{ - "foo": 12, - "bar": 12, - }, - }, - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "top.4.foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "top.4.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "3", - }, - "top.24.foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "12", - }, - "top.24.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "12", - }, - }, - }, - Key: "top", - NewValue: NewSet(testSetFunc, []interface{}{ - map[string]interface{}{ - "foo": 1, - "bar": 4, - }, - map[string]interface{}{ - "foo": 13, - "bar": 12, - }, - map[string]interface{}{ - "foo": 21, - "bar": 22, - }, - }), - Expected: &terraform.InstanceDiff{ - Attributes: func() map[string]*terraform.ResourceAttrDiff { - result := make(map[string]*terraform.ResourceAttrDiff) - if computed { - result["top.#"] = &terraform.ResourceAttrDiff{ - Old: "2", - New: "", - NewComputed: true, - } - } else { - result["top.#"] = &terraform.ResourceAttrDiff{ - Old: "2", - New: "3", - } - result["top.5.foo"] = &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - } - result["top.5.bar"] = &terraform.ResourceAttrDiff{ - Old: "", - New: "4", - } - result["top.25.foo"] = &terraform.ResourceAttrDiff{ - Old: "", - New: "13", - } - result["top.25.bar"] = &terraform.ResourceAttrDiff{ - Old: "", - New: "12", - } - result["top.43.foo"] = &terraform.ResourceAttrDiff{ - Old: "", - New: "21", - } - result["top.43.bar"] = &terraform.ResourceAttrDiff{ - Old: "", - New: "22", - } - } - return result - }(), - }, - }, - resourceDiffTestCase{ - Name: "primitive, no diff, no refresh", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{}), - Diff: &terraform.InstanceDiff{Attributes: map[string]*terraform.ResourceAttrDiff{}}, - Key: "foo", - NewValue: "baz", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: func() string { - if computed { - return "" - } - return "baz" - }(), - NewComputed: computed, - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "non-computed key, should error", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Required: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "foo", - NewValue: "qux", - ExpectedError: true, - }, - resourceDiffTestCase{ - Name: "bad key, should error", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Required: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "bad", - NewValue: "qux", - ExpectedError: true, - }, - resourceDiffTestCase{ - // NOTE: This case is technically impossible in the current - // implementation, because optional+computed values never show up in the - // diff, and we actually clear existing diffs when SetNew or - // SetNewComputed is run. This test is here to ensure that if either of - // these behaviors change that we don't introduce regressions. - Name: "NewRemoved in diff for Optional and Computed, should be fully overridden", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{}), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "", - NewRemoved: true, - }, - }, - }, - Key: "foo", - NewValue: "qux", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: func() string { - if computed { - return "" - } - return "qux" - }(), - NewComputed: computed, - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "NewComputed should always propagate", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "", - }, - ID: "pre-existing", - }, - Config: testConfig(t, map[string]interface{}{}), - Diff: &terraform.InstanceDiff{Attributes: map[string]*terraform.ResourceAttrDiff{}}, - Key: "foo", - NewValue: "", - Expected: &terraform.InstanceDiff{ - Attributes: func() map[string]*terraform.ResourceAttrDiff { - if computed { - return map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - NewComputed: computed, - }, - } - } - return map[string]*terraform.ResourceAttrDiff{} - }(), - }, - }, - } -} - -func TestSetNew(t *testing.T) { - testCases := testDiffCases(t, "", 0, false) - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - m := schemaMap(tc.Schema) - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) - err := d.SetNew(tc.Key, tc.NewValue) - switch { - case err != nil && !tc.ExpectedError: - t.Fatalf("bad: %s", err) - case err == nil && tc.ExpectedError: - t.Fatalf("Expected error, got none") - case err != nil && tc.ExpectedError: - return - } - for _, k := range d.UpdatedKeys() { - if err := m.diff(k, m[k], tc.Diff, d, false); err != nil { - t.Fatalf("bad: %s", err) - } - } - if !reflect.DeepEqual(tc.Expected, tc.Diff) { - t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff)) - } - }) - } -} - -func TestSetNewComputed(t *testing.T) { - testCases := testDiffCases(t, "", 0, true) - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - m := schemaMap(tc.Schema) - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) - err := d.SetNewComputed(tc.Key) - switch { - case err != nil && !tc.ExpectedError: - t.Fatalf("bad: %s", err) - case err == nil && tc.ExpectedError: - t.Fatalf("Expected error, got none") - case err != nil && tc.ExpectedError: - return - } - for _, k := range d.UpdatedKeys() { - if err := m.diff(k, m[k], tc.Diff, d, false); err != nil { - t.Fatalf("bad: %s", err) - } - } - if !reflect.DeepEqual(tc.Expected, tc.Diff) { - t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff)) - } - }) - } -} - -func TestForceNew(t *testing.T) { - cases := []resourceDiffTestCase{ - resourceDiffTestCase{ - Name: "basic primitive diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "foo", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - RequiresNew: true, - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "no change, should error", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "bar", - }), - ExpectedError: true, - }, - resourceDiffTestCase{ - Name: "basic primitive, non-computed key", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Required: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "foo", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - RequiresNew: true, - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "nested field", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Required: true, - MaxItems: 1, - Elem: &Resource{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeString, - Optional: true, - }, - "baz": { - Type: TypeString, - Optional: true, - }, - }, - }, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo.#": "1", - "foo.0.bar": "abc", - "foo.0.baz": "xyz", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": []interface{}{ - map[string]interface{}{ - "bar": "abcdefg", - "baz": "changed", - }, - }, - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.0.bar": &terraform.ResourceAttrDiff{ - Old: "abc", - New: "abcdefg", - }, - "foo.0.baz": &terraform.ResourceAttrDiff{ - Old: "xyz", - New: "changed", - }, - }, - }, - Key: "foo.0.baz", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.0.bar": &terraform.ResourceAttrDiff{ - Old: "abc", - New: "abcdefg", - }, - "foo.0.baz": &terraform.ResourceAttrDiff{ - Old: "xyz", - New: "changed", - RequiresNew: true, - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "preserve NewRemoved on existing diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{}), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "", - NewRemoved: true, - }, - }, - }, - Key: "foo", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "", - RequiresNew: true, - NewRemoved: true, - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "nested field, preserve original diff without zero values", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Required: true, - MaxItems: 1, - Elem: &Resource{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeString, - Optional: true, - }, - "baz": { - Type: TypeInt, - Optional: true, - }, - }, - }, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo.#": "1", - "foo.0.bar": "abc", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": []interface{}{ - map[string]interface{}{ - "bar": "abcdefg", - }, - }, - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.0.bar": &terraform.ResourceAttrDiff{ - Old: "abc", - New: "abcdefg", - }, - }, - }, - Key: "foo.0.bar", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.0.bar": &terraform.ResourceAttrDiff{ - Old: "abc", - New: "abcdefg", - RequiresNew: true, - }, - }, - }, - }, - } - for _, tc := range cases { - t.Run(tc.Name, func(t *testing.T) { - m := schemaMap(tc.Schema) - d := newResourceDiff(m, tc.Config, tc.State, tc.Diff) - err := d.ForceNew(tc.Key) - switch { - case err != nil && !tc.ExpectedError: - t.Fatalf("bad: %s", err) - case err == nil && tc.ExpectedError: - t.Fatalf("Expected error, got none") - case err != nil && tc.ExpectedError: - return - } - for _, k := range d.UpdatedKeys() { - if err := m.diff(k, m[k], tc.Diff, d, false); err != nil { - t.Fatalf("bad: %s", err) - } - } - if !reflect.DeepEqual(tc.Expected, tc.Diff) { - t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff)) - } - }) - } -} - -func TestClear(t *testing.T) { - cases := []resourceDiffTestCase{ - resourceDiffTestCase{ - Name: "basic primitive diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "foo", - Expected: &terraform.InstanceDiff{Attributes: map[string]*terraform.ResourceAttrDiff{}}, - }, - resourceDiffTestCase{ - Name: "non-computed key, should error", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Required: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "foo", - ExpectedError: true, - }, - resourceDiffTestCase{ - Name: "multi-value, one removed", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - "one": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - "one": "two", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - "one": "three", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - "one": &terraform.ResourceAttrDiff{ - Old: "two", - New: "three", - }, - }, - }, - Key: "one", - Expected: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - }, - resourceDiffTestCase{ - Name: "basic sub-block diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "bar": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - "baz": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo.0.bar": "bar1", - "foo.0.baz": "baz1", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": []interface{}{ - map[string]interface{}{ - "bar": "bar2", - "baz": "baz1", - }, - }, - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.0.bar": &terraform.ResourceAttrDiff{ - Old: "bar1", - New: "bar2", - }, - }, - }, - Key: "foo.0.bar", - Expected: &terraform.InstanceDiff{Attributes: map[string]*terraform.ResourceAttrDiff{}}, - }, - resourceDiffTestCase{ - Name: "sub-block diff only partial clear", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "bar": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - "baz": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo.0.bar": "bar1", - "foo.0.baz": "baz1", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": []interface{}{ - map[string]interface{}{ - "bar": "bar2", - "baz": "baz2", - }, - }, - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.0.bar": &terraform.ResourceAttrDiff{ - Old: "bar1", - New: "bar2", - }, - "foo.0.baz": &terraform.ResourceAttrDiff{ - Old: "baz1", - New: "baz2", - }, - }, - }, - Key: "foo.0.bar", - Expected: &terraform.InstanceDiff{Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo.0.baz": &terraform.ResourceAttrDiff{ - Old: "baz1", - New: "baz2", - }, - }}, - }, - } - for _, tc := range cases { - t.Run(tc.Name, func(t *testing.T) { - m := schemaMap(tc.Schema) - d := newResourceDiff(m, tc.Config, tc.State, tc.Diff) - err := d.Clear(tc.Key) - switch { - case err != nil && !tc.ExpectedError: - t.Fatalf("bad: %s", err) - case err == nil && tc.ExpectedError: - t.Fatalf("Expected error, got none") - case err != nil && tc.ExpectedError: - return - } - for _, k := range d.UpdatedKeys() { - if err := m.diff(k, m[k], tc.Diff, d, false); err != nil { - t.Fatalf("bad: %s", err) - } - } - if !reflect.DeepEqual(tc.Expected, tc.Diff) { - t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff)) - } - }) - } -} - -func TestGetChangedKeysPrefix(t *testing.T) { - cases := []resourceDiffTestCase{ - resourceDiffTestCase{ - Name: "basic primitive diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "foo": "baz", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - }, - }, - Key: "foo", - ExpectedKeys: []string{ - "foo", - }, - }, - resourceDiffTestCase{ - Name: "nested field filtering", - Schema: map[string]*Schema{ - "testfield": &Schema{ - Type: TypeString, - Required: true, - }, - "foo": &Schema{ - Type: TypeList, - Required: true, - MaxItems: 1, - Elem: &Resource{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeString, - Optional: true, - }, - "baz": { - Type: TypeString, - Optional: true, - }, - }, - }, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "testfield": "blablah", - "foo.#": "1", - "foo.0.bar": "abc", - "foo.0.baz": "xyz", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "testfield": "modified", - "foo": []interface{}{ - map[string]interface{}{ - "bar": "abcdefg", - "baz": "changed", - }, - }, - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "testfield": &terraform.ResourceAttrDiff{ - Old: "blablah", - New: "modified", - }, - "foo.0.bar": &terraform.ResourceAttrDiff{ - Old: "abc", - New: "abcdefg", - }, - "foo.0.baz": &terraform.ResourceAttrDiff{ - Old: "xyz", - New: "changed", - }, - }, - }, - Key: "foo", - ExpectedKeys: []string{ - "foo.0.bar", - "foo.0.baz", - }, - }, - } - for _, tc := range cases { - t.Run(tc.Name, func(t *testing.T) { - m := schemaMap(tc.Schema) - d := newResourceDiff(m, tc.Config, tc.State, tc.Diff) - keys := d.GetChangedKeysPrefix(tc.Key) - - for _, k := range d.UpdatedKeys() { - if err := m.diff(k, m[k], tc.Diff, d, false); err != nil { - t.Fatalf("bad: %s", err) - } - } - - sort.Strings(keys) - - if !reflect.DeepEqual(tc.ExpectedKeys, keys) { - t.Fatalf("Expected %s, got %s", spew.Sdump(tc.ExpectedKeys), spew.Sdump(keys)) - } - }) - } -} - -func TestResourceDiffGetOkExists(t *testing.T) { - cases := []struct { - Name string - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - Value interface{} - Ok bool - }{ - /* - * Primitives - */ - { - Name: "string-literal-empty", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "", - }, - }, - }, - - Key: "availability_zone", - Value: "", - Ok: true, - }, - - { - Name: "string-computed-empty", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - - Key: "availability_zone", - Value: "", - Ok: false, - }, - - { - Name: "string-optional-computed-nil-diff", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - Config: nil, - - Diff: nil, - - Key: "availability_zone", - Value: "", - Ok: false, - }, - - /* - * Lists - */ - - { - Name: "list-optional", - Schema: map[string]*Schema{ - "ports": { - Type: TypeList, - Optional: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - Config: nil, - - Diff: nil, - - Key: "ports", - Value: []interface{}{}, - Ok: false, - }, - - /* - * Map - */ - - { - Name: "map-optional", - Schema: map[string]*Schema{ - "ports": { - Type: TypeMap, - Optional: true, - }, - }, - - State: nil, - Config: nil, - - Diff: nil, - - Key: "ports", - Value: map[string]interface{}{}, - Ok: false, - }, - - /* - * Set - */ - - { - Name: "set-optional", - Schema: map[string]*Schema{ - "ports": { - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { return a.(int) }, - }, - }, - - State: nil, - Config: nil, - - Diff: nil, - - Key: "ports", - Value: []interface{}{}, - Ok: false, - }, - - { - Name: "set-optional-key", - Schema: map[string]*Schema{ - "ports": { - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { return a.(int) }, - }, - }, - - State: nil, - Config: nil, - - Diff: nil, - - Key: "ports.0", - Value: 0, - Ok: false, - }, - - { - Name: "bool-literal-empty", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - Config: nil, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "", - }, - }, - }, - - Key: "availability_zone", - Value: false, - Ok: true, - }, - - { - Name: "bool-literal-set", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - New: "true", - }, - }, - }, - - Key: "availability_zone", - Value: true, - Ok: true, - }, - { - Name: "value-in-config", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "availability_zone": "foo", - }), - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - }, - - Key: "availability_zone", - Value: "foo", - Ok: true, - }, - { - Name: "new-value-in-config", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - }, - }, - - State: nil, - Config: testConfig(t, map[string]interface{}{ - "availability_zone": "foo", - }), - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "foo", - }, - }, - }, - - Key: "availability_zone", - Value: "foo", - Ok: true, - }, - { - Name: "optional-computed-value-in-config", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "availability_zone": "bar", - }), - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "foo", - New: "bar", - }, - }, - }, - - Key: "availability_zone", - Value: "bar", - Ok: true, - }, - { - Name: "removed-value", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig(t, map[string]interface{}{}), - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "foo", - New: "", - NewRemoved: true, - }, - }, - }, - - Key: "availability_zone", - Value: "", - Ok: true, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) - - v, ok := d.GetOkExists(tc.Key) - if s, ok := v.(*Set); ok { - v = s.List() - } - - if !reflect.DeepEqual(v, tc.Value) { - t.Fatalf("Bad %s: \n%#v", tc.Name, v) - } - if ok != tc.Ok { - t.Fatalf("%s: expected ok: %t, got: %t", tc.Name, tc.Ok, ok) - } - }) - } -} - -func TestResourceDiffGetOkExistsSetNew(t *testing.T) { - tc := struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - Value interface{} - Ok bool - }{ - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - }, - - Key: "availability_zone", - Value: "foobar", - Ok: true, - } - - d := newResourceDiff(tc.Schema, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff) - d.SetNew(tc.Key, tc.Value) - - v, ok := d.GetOkExists(tc.Key) - if s, ok := v.(*Set); ok { - v = s.List() - } - - if !reflect.DeepEqual(v, tc.Value) { - t.Fatalf("Bad: \n%#v", v) - } - if ok != tc.Ok { - t.Fatalf("expected ok: %t, got: %t", tc.Ok, ok) - } -} - -func TestResourceDiffGetOkExistsSetNewComputed(t *testing.T) { - tc := struct { - Schema map[string]*Schema - State *terraform.InstanceState - Diff *terraform.InstanceDiff - Key string - Value interface{} - Ok bool - }{ - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - }, - - Key: "availability_zone", - Value: "foobar", - Ok: false, - } - - d := newResourceDiff(tc.Schema, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff) - d.SetNewComputed(tc.Key) - - _, ok := d.GetOkExists(tc.Key) - - if ok != tc.Ok { - t.Fatalf("expected ok: %t, got: %t", tc.Ok, ok) - } -} - -func TestResourceDiffNewValueKnown(t *testing.T) { - cases := []struct { - Name string - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - Expected bool - }{ - { - Name: "in config, no state", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - }, - }, - State: nil, - Config: testConfig(t, map[string]interface{}{ - "availability_zone": "foo", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "foo", - }, - }, - }, - Key: "availability_zone", - Expected: true, - }, - { - Name: "in config, has state, no diff", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "availability_zone": "foo", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - }, - Key: "availability_zone", - Expected: true, - }, - { - Name: "computed attribute, in state, no diff", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig(t, map[string]interface{}{}), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - }, - Key: "availability_zone", - Expected: true, - }, - { - Name: "optional and computed attribute, in state, no config", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig(t, map[string]interface{}{}), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - }, - Key: "availability_zone", - Expected: true, - }, - { - Name: "optional and computed attribute, in state, with config", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig(t, map[string]interface{}{ - "availability_zone": "foo", - }), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - }, - Key: "availability_zone", - Expected: true, - }, - { - Name: "computed value, through config reader", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig( - t, - map[string]interface{}{ - "availability_zone": hcl2shim.UnknownVariableValue, - }, - ), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - }, - Key: "availability_zone", - Expected: false, - }, - { - Name: "computed value, through diff reader", - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig( - t, - map[string]interface{}{ - "availability_zone": hcl2shim.UnknownVariableValue, - }, - ), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "foo", - New: "", - NewComputed: true, - }, - }, - }, - Key: "availability_zone", - Expected: false, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) - - actual := d.NewValueKnown(tc.Key) - if tc.Expected != actual { - t.Fatalf("%s: expected ok: %t, got: %t", tc.Name, tc.Expected, actual) - } - }) - } -} - -func TestResourceDiffNewValueKnownSetNew(t *testing.T) { - tc := struct { - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - Value interface{} - Expected bool - }{ - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig( - t, - map[string]interface{}{ - "availability_zone": hcl2shim.UnknownVariableValue, - }, - ), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "foo", - New: "", - NewComputed: true, - }, - }, - }, - Key: "availability_zone", - Value: "bar", - Expected: true, - } - - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) - d.SetNew(tc.Key, tc.Value) - - actual := d.NewValueKnown(tc.Key) - if tc.Expected != actual { - t.Fatalf("expected ok: %t, got: %t", tc.Expected, actual) - } -} - -func TestResourceDiffNewValueKnownSetNewComputed(t *testing.T) { - tc := struct { - Schema map[string]*Schema - State *terraform.InstanceState - Config *terraform.ResourceConfig - Diff *terraform.InstanceDiff - Key string - Expected bool - }{ - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Computed: true, - }, - }, - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - Config: testConfig(t, map[string]interface{}{}), - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - }, - Key: "availability_zone", - Expected: false, - } - - d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff) - d.SetNewComputed(tc.Key) - - actual := d.NewValueKnown(tc.Key) - if tc.Expected != actual { - t.Fatalf("expected ok: %t, got: %t", tc.Expected, actual) - } -} diff --git a/helper/schema/resource_importer.go b/helper/schema/resource_importer.go deleted file mode 100644 index 5dada3caf..000000000 --- a/helper/schema/resource_importer.go +++ /dev/null @@ -1,52 +0,0 @@ -package schema - -// ResourceImporter defines how a resource is imported in Terraform. This -// can be set onto a Resource struct to make it Importable. Not all resources -// have to be importable; if a Resource doesn't have a ResourceImporter then -// it won't be importable. -// -// "Importing" in Terraform is the process of taking an already-created -// resource and bringing it under Terraform management. This can include -// updating Terraform state, generating Terraform configuration, etc. -type ResourceImporter struct { - // The functions below must all be implemented for importing to work. - - // State is called to convert an ID to one or more InstanceState to - // insert into the Terraform state. If this isn't specified, then - // the ID is passed straight through. - State StateFunc -} - -// StateFunc is the function called to import a resource into the -// Terraform state. It is given a ResourceData with only ID set. This -// ID is going to be an arbitrary value given by the user and may not map -// directly to the ID format that the resource expects, so that should -// be validated. -// -// This should return a slice of ResourceData that turn into the state -// that was imported. This might be as simple as returning only the argument -// that was given to the function. In other cases (such as AWS security groups), -// an import may fan out to multiple resources and this will have to return -// multiple. -// -// To create the ResourceData structures for other resource types (if -// you have to), instantiate your resource and call the Data function. -type StateFunc func(*ResourceData, interface{}) ([]*ResourceData, error) - -// InternalValidate should be called to validate the structure of this -// importer. This should be called in a unit test. -// -// Resource.InternalValidate() will automatically call this, so this doesn't -// need to be called manually. Further, Resource.InternalValidate() is -// automatically called by Provider.InternalValidate(), so you only need -// to internal validate the provider. -func (r *ResourceImporter) InternalValidate() error { - return nil -} - -// ImportStatePassthrough is an implementation of StateFunc that can be -// used to simply pass the ID directly through. This should be used only -// in the case that an ID-only refresh is possible. -func ImportStatePassthrough(d *ResourceData, m interface{}) ([]*ResourceData, error) { - return []*ResourceData{d}, nil -} diff --git a/helper/schema/resource_test.go b/helper/schema/resource_test.go deleted file mode 100644 index a532dba39..000000000 --- a/helper/schema/resource_test.go +++ /dev/null @@ -1,1687 +0,0 @@ -package schema - -import ( - "encoding/json" - "fmt" - "reflect" - "strconv" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/terraform" - - "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" -) - -func TestResourceApply_create(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - called := false - r.Create = func(d *ResourceData, m interface{}) error { - called = true - d.SetId("foo") - return nil - } - - var s *terraform.InstanceState = nil - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "42", - }, - }, - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !called { - t.Fatal("not called") - } - - expected := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "42", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceApply_Timeout_state(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(40 * time.Minute), - Update: DefaultTimeout(80 * time.Minute), - Delete: DefaultTimeout(40 * time.Minute), - }, - } - - called := false - r.Create = func(d *ResourceData, m interface{}) error { - called = true - d.SetId("foo") - return nil - } - - var s *terraform.InstanceState = nil - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "42", - }, - }, - } - - diffTimeout := &ResourceTimeout{ - Create: DefaultTimeout(40 * time.Minute), - Update: DefaultTimeout(80 * time.Minute), - Delete: DefaultTimeout(40 * time.Minute), - } - - if err := diffTimeout.DiffEncode(d); err != nil { - t.Fatalf("Error encoding timeout to diff: %s", err) - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !called { - t.Fatal("not called") - } - - expected := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "42", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - TimeoutKey: expectedForValues(40, 0, 80, 40, 0), - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Not equal in Timeout State:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta) - } -} - -// Regression test to ensure that the meta data is read from state, if a -// resource is destroyed and the timeout meta is no longer available from the -// config -func TestResourceApply_Timeout_destroy(t *testing.T) { - timeouts := &ResourceTimeout{ - Create: DefaultTimeout(40 * time.Minute), - Update: DefaultTimeout(80 * time.Minute), - Delete: DefaultTimeout(40 * time.Minute), - } - - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: timeouts, - } - - called := false - var delTimeout time.Duration - r.Delete = func(d *ResourceData, m interface{}) error { - delTimeout = d.Timeout(TimeoutDelete) - called = true - return nil - } - - s := &terraform.InstanceState{ - ID: "bar", - } - - if err := timeouts.StateEncode(s); err != nil { - t.Fatalf("Error encoding to state: %s", err) - } - - d := &terraform.InstanceDiff{ - Destroy: true, - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !called { - t.Fatal("delete not called") - } - - if *timeouts.Delete != delTimeout { - t.Fatalf("timeouts don't match, expected (%#v), got (%#v)", timeouts.Delete, delTimeout) - } - - if actual != nil { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceDiff_Timeout_diff(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(40 * time.Minute), - Update: DefaultTimeout(80 * time.Minute), - Delete: DefaultTimeout(40 * time.Minute), - }, - } - - r.Create = func(d *ResourceData, m interface{}) error { - d.SetId("foo") - return nil - } - - conf := terraform.NewResourceConfigRaw( - map[string]interface{}{ - "foo": 42, - TimeoutsConfigKey: map[string]interface{}{ - "create": "2h", - }, - }, - ) - var s *terraform.InstanceState - - actual, err := r.Diff(s, conf, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "42", - }, - }, - } - - diffTimeout := &ResourceTimeout{ - Create: DefaultTimeout(120 * time.Minute), - Update: DefaultTimeout(80 * time.Minute), - Delete: DefaultTimeout(40 * time.Minute), - } - - if err := diffTimeout.DiffEncode(expected); err != nil { - t.Fatalf("Error encoding timeout to diff: %s", err) - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Not equal Meta in Timeout Diff:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta) - } -} - -func TestResourceDiff_CustomizeFunc(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - var called bool - - r.CustomizeDiff = func(d *ResourceDiff, m interface{}) error { - called = true - return nil - } - - conf := terraform.NewResourceConfigRaw( - map[string]interface{}{ - "foo": 42, - }, - ) - - var s *terraform.InstanceState - - _, err := r.Diff(s, conf, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !called { - t.Fatalf("diff customization not called") - } -} - -func TestResourceApply_destroy(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - called := false - r.Delete = func(d *ResourceData, m interface{}) error { - called = true - return nil - } - - s := &terraform.InstanceState{ - ID: "bar", - } - - d := &terraform.InstanceDiff{ - Destroy: true, - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !called { - t.Fatal("delete not called") - } - - if actual != nil { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceApply_destroyCreate(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - - "tags": &Schema{ - Type: TypeMap, - Optional: true, - Computed: true, - }, - }, - } - - change := false - r.Create = func(d *ResourceData, m interface{}) error { - change = d.HasChange("tags") - d.SetId("foo") - return nil - } - r.Delete = func(d *ResourceData, m interface{}) error { - return nil - } - - var s *terraform.InstanceState = &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "bar", - "tags.Name": "foo", - }, - } - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "42", - RequiresNew: true, - }, - "tags.Name": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "foo", - RequiresNew: true, - }, - }, - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !change { - t.Fatal("should have change") - } - - expected := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "42", - "tags.%": "1", - "tags.Name": "foo", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceApply_destroyPartial(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - SchemaVersion: 3, - } - - r.Delete = func(d *ResourceData, m interface{}) error { - d.Set("foo", 42) - return fmt.Errorf("some error") - } - - s := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "12", - }, - } - - d := &terraform.InstanceDiff{ - Destroy: true, - } - - actual, err := r.Apply(s, d, nil) - if err == nil { - t.Fatal("should error") - } - - expected := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "foo": "42", - }, - Meta: map[string]interface{}{ - "schema_version": "3", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("expected:\n%#v\n\ngot:\n%#v", expected, actual) - } -} - -func TestResourceApply_update(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Update = func(d *ResourceData, m interface{}) error { - d.Set("foo", 42) - return nil - } - - s := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "12", - }, - } - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "13", - }, - }, - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "42", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceApply_updateNoCallback(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Update = nil - - s := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "12", - }, - } - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "13", - }, - }, - } - - actual, err := r.Apply(s, d, nil) - if err == nil { - t.Fatal("should error") - } - - expected := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "12", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceApply_isNewResource(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - } - - updateFunc := func(d *ResourceData, m interface{}) error { - d.Set("foo", "updated") - if d.IsNewResource() { - d.Set("foo", "new-resource") - } - return nil - } - r.Create = func(d *ResourceData, m interface{}) error { - d.SetId("foo") - d.Set("foo", "created") - return updateFunc(d, m) - } - r.Update = updateFunc - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "bla-blah", - }, - }, - } - - // positive test - var s *terraform.InstanceState = nil - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "new-resource", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("actual: %#v\nexpected: %#v", - actual, expected) - } - - // negative test - s = &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "new-resource", - }, - } - - actual, err = r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected = &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "updated", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("actual: %#v\nexpected: %#v", - actual, expected) - } -} - -func TestResourceInternalValidate(t *testing.T) { - cases := []struct { - In *Resource - Writable bool - Err bool - }{ - 0: { - nil, - true, - true, - }, - - // No optional and no required - 1: { - &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - Required: true, - }, - }, - }, - true, - true, - }, - - // Update undefined for non-ForceNew field - 2: { - &Resource{ - Create: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "boo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - }, - true, - true, - }, - - // Update defined for ForceNew field - 3: { - &Resource{ - Create: func(d *ResourceData, meta interface{}) error { return nil }, - Update: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "goo": &Schema{ - Type: TypeInt, - Optional: true, - ForceNew: true, - }, - }, - }, - true, - true, - }, - - // non-writable doesn't need Update, Create or Delete - 4: { - &Resource{ - Schema: map[string]*Schema{ - "goo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - }, - false, - false, - }, - - // non-writable *must not* have Create - 5: { - &Resource{ - Create: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "goo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - }, - false, - true, - }, - - // writable must have Read - 6: { - &Resource{ - Create: func(d *ResourceData, meta interface{}) error { return nil }, - Update: func(d *ResourceData, meta interface{}) error { return nil }, - Delete: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "goo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - }, - true, - true, - }, - - // writable must have Delete - 7: { - &Resource{ - Create: func(d *ResourceData, meta interface{}) error { return nil }, - Read: func(d *ResourceData, meta interface{}) error { return nil }, - Update: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "goo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - }, - true, - true, - }, - - 8: { // Reserved name at root should be disallowed - &Resource{ - Create: func(d *ResourceData, meta interface{}) error { return nil }, - Read: func(d *ResourceData, meta interface{}) error { return nil }, - Update: func(d *ResourceData, meta interface{}) error { return nil }, - Delete: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "count": { - Type: TypeInt, - Optional: true, - }, - }, - }, - true, - true, - }, - - 9: { // Reserved name at nested levels should be allowed - &Resource{ - Create: func(d *ResourceData, meta interface{}) error { return nil }, - Read: func(d *ResourceData, meta interface{}) error { return nil }, - Update: func(d *ResourceData, meta interface{}) error { return nil }, - Delete: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "parent_list": &Schema{ - Type: TypeString, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "provisioner": { - Type: TypeString, - Optional: true, - }, - }, - }, - }, - }, - }, - true, - false, - }, - - 10: { // Provider reserved name should be allowed in resource - &Resource{ - Create: func(d *ResourceData, meta interface{}) error { return nil }, - Read: func(d *ResourceData, meta interface{}) error { return nil }, - Update: func(d *ResourceData, meta interface{}) error { return nil }, - Delete: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "alias": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - }, - true, - false, - }, - - 11: { // ID should be allowed in data source - &Resource{ - Read: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "id": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - }, - false, - false, - }, - - 12: { // Deprecated ID should be allowed in resource - &Resource{ - Create: func(d *ResourceData, meta interface{}) error { return nil }, - Read: func(d *ResourceData, meta interface{}) error { return nil }, - Update: func(d *ResourceData, meta interface{}) error { return nil }, - Delete: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "id": &Schema{ - Type: TypeString, - Optional: true, - Deprecated: "Use x_id instead", - }, - }, - }, - true, - false, - }, - - 13: { // non-writable must not define CustomizeDiff - &Resource{ - Read: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "goo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - CustomizeDiff: func(*ResourceDiff, interface{}) error { return nil }, - }, - false, - true, - }, - 14: { // Deprecated resource - &Resource{ - Read: func(d *ResourceData, meta interface{}) error { return nil }, - Schema: map[string]*Schema{ - "goo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - DeprecationMessage: "This resource has been deprecated.", - }, - true, - true, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { - sm := schemaMap{} - if tc.In != nil { - sm = schemaMap(tc.In.Schema) - } - - err := tc.In.InternalValidate(sm, tc.Writable) - if err != nil && !tc.Err { - t.Fatalf("%d: expected validation to pass: %s", i, err) - } - if err == nil && tc.Err { - t.Fatalf("%d: expected validation to fail", i) - } - }) - } -} - -func TestResourceRefresh(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Read = func(d *ResourceData, m interface{}) error { - if m != 42 { - return fmt.Errorf("meta not passed") - } - - return d.Set("foo", d.Get("foo").(int)+1) - } - - s := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "12", - }, - } - - expected := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "foo": "13", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - }, - } - - actual, err := r.Refresh(s, 42) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceRefresh_blankId(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Read = func(d *ResourceData, m interface{}) error { - d.SetId("foo") - return nil - } - - s := &terraform.InstanceState{ - ID: "", - Attributes: map[string]string{}, - } - - actual, err := r.Refresh(s, 42) - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != nil { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceRefresh_delete(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Read = func(d *ResourceData, m interface{}) error { - d.SetId("") - return nil - } - - s := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "12", - }, - } - - actual, err := r.Refresh(s, 42) - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual != nil { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceRefresh_existsError(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Exists = func(*ResourceData, interface{}) (bool, error) { - return false, fmt.Errorf("error") - } - - r.Read = func(d *ResourceData, m interface{}) error { - panic("shouldn't be called") - } - - s := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "12", - }, - } - - actual, err := r.Refresh(s, 42) - if err == nil { - t.Fatalf("should error") - } - if !reflect.DeepEqual(actual, s) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestResourceRefresh_noExists(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Exists = func(*ResourceData, interface{}) (bool, error) { - return false, nil - } - - r.Read = func(d *ResourceData, m interface{}) error { - panic("shouldn't be called") - } - - s := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "12", - }, - } - - actual, err := r.Refresh(s, 42) - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != nil { - t.Fatalf("should have no state") - } -} - -func TestResourceRefresh_needsMigration(t *testing.T) { - // Schema v2 it deals only in newfoo, which tracks foo as an int - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "newfoo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Read = func(d *ResourceData, m interface{}) error { - return d.Set("newfoo", d.Get("newfoo").(int)+1) - } - - r.MigrateState = func( - v int, - s *terraform.InstanceState, - meta interface{}) (*terraform.InstanceState, error) { - // Real state migration functions will probably switch on this value, - // but we'll just assert on it for now. - if v != 1 { - t.Fatalf("Expected StateSchemaVersion to be 1, got %d", v) - } - - if meta != 42 { - t.Fatal("Expected meta to be passed through to the migration function") - } - - oldfoo, err := strconv.ParseFloat(s.Attributes["oldfoo"], 64) - if err != nil { - t.Fatalf("err: %#v", err) - } - s.Attributes["newfoo"] = strconv.Itoa(int(oldfoo * 10)) - delete(s.Attributes, "oldfoo") - - return s, nil - } - - // State is v1 and deals in oldfoo, which tracked foo as a float at 1/10th - // the scale of newfoo - s := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "oldfoo": "1.2", - }, - Meta: map[string]interface{}{ - "schema_version": "1", - }, - } - - actual, err := r.Refresh(s, 42) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "newfoo": "13", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) - } -} - -func TestResourceRefresh_noMigrationNeeded(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "newfoo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Read = func(d *ResourceData, m interface{}) error { - return d.Set("newfoo", d.Get("newfoo").(int)+1) - } - - r.MigrateState = func( - v int, - s *terraform.InstanceState, - meta interface{}) (*terraform.InstanceState, error) { - t.Fatal("Migrate function shouldn't be called!") - return nil, nil - } - - s := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "newfoo": "12", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - }, - } - - actual, err := r.Refresh(s, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "newfoo": "13", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) - } -} - -func TestResourceRefresh_stateSchemaVersionUnset(t *testing.T) { - r := &Resource{ - // Version 1 > Version 0 - SchemaVersion: 1, - Schema: map[string]*Schema{ - "newfoo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Read = func(d *ResourceData, m interface{}) error { - return d.Set("newfoo", d.Get("newfoo").(int)+1) - } - - r.MigrateState = func( - v int, - s *terraform.InstanceState, - meta interface{}) (*terraform.InstanceState, error) { - s.Attributes["newfoo"] = s.Attributes["oldfoo"] - return s, nil - } - - s := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "oldfoo": "12", - }, - } - - actual, err := r.Refresh(s, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "newfoo": "13", - }, - Meta: map[string]interface{}{ - "schema_version": "1", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) - } -} - -func TestResourceRefresh_migrateStateErr(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "newfoo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.Read = func(d *ResourceData, m interface{}) error { - t.Fatal("Read should never be called!") - return nil - } - - r.MigrateState = func( - v int, - s *terraform.InstanceState, - meta interface{}) (*terraform.InstanceState, error) { - return s, fmt.Errorf("triggering an error") - } - - s := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "oldfoo": "12", - }, - } - - _, err := r.Refresh(s, nil) - if err == nil { - t.Fatal("expected error, but got none!") - } -} - -func TestResourceData(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - state := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "42", - }, - } - - data := r.Data(state) - if data.Id() != "foo" { - t.Fatalf("err: %s", data.Id()) - } - if v := data.Get("foo"); v != 42 { - t.Fatalf("bad: %#v", v) - } - - // Set expectations - state.Meta = map[string]interface{}{ - "schema_version": "2", - } - - result := data.State() - if !reflect.DeepEqual(result, state) { - t.Fatalf("bad: %#v", result) - } -} - -func TestResourceData_blank(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - data := r.Data(nil) - if data.Id() != "" { - t.Fatalf("err: %s", data.Id()) - } - if v := data.Get("foo"); v != 0 { - t.Fatalf("bad: %#v", v) - } -} - -func TestResourceData_timeouts(t *testing.T) { - one := 1 * time.Second - two := 2 * time.Second - three := 3 * time.Second - four := 4 * time.Second - five := 5 * time.Second - - timeouts := &ResourceTimeout{ - Create: &one, - Read: &two, - Update: &three, - Delete: &four, - Default: &five, - } - - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: timeouts, - } - - data := r.Data(nil) - if data.Id() != "" { - t.Fatalf("err: %s", data.Id()) - } - - if !reflect.DeepEqual(timeouts, data.timeouts) { - t.Fatalf("incorrect ResourceData timeouts: %#v\n", *data.timeouts) - } -} - -func TestResource_UpgradeState(t *testing.T) { - // While this really only calls itself and therefore doesn't test any of - // the Resource code directly, it still serves as an example of registering - // a StateUpgrader. - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "newfoo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - r.StateUpgraders = []StateUpgrader{ - { - Version: 1, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - "oldfoo": cty.Number, - }), - Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - - oldfoo, ok := m["oldfoo"].(float64) - if !ok { - t.Fatalf("expected 1.2, got %#v", m["oldfoo"]) - } - m["newfoo"] = int(oldfoo * 10) - delete(m, "oldfoo") - - return m, nil - }, - }, - } - - oldStateAttrs := map[string]string{ - "id": "bar", - "oldfoo": "1.2", - } - - // convert the legacy flatmap state to the json equivalent - ty := r.StateUpgraders[0].Type - val, err := hcl2shim.HCL2ValueFromFlatmap(oldStateAttrs, ty) - if err != nil { - t.Fatal(err) - } - js, err := ctyjson.Marshal(val, ty) - if err != nil { - t.Fatal(err) - } - - // unmarshal the state using the json default types - var m map[string]interface{} - if err := json.Unmarshal(js, &m); err != nil { - t.Fatal(err) - } - - actual, err := r.StateUpgraders[0].Upgrade(m, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := map[string]interface{}{ - "id": "bar", - "newfoo": 12, - } - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("expected: %#v\ngot: %#v\n", expected, actual) - } -} - -func TestResource_ValidateUpgradeState(t *testing.T) { - r := &Resource{ - SchemaVersion: 3, - Schema: map[string]*Schema{ - "newfoo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - if err := r.InternalValidate(nil, true); err != nil { - t.Fatal(err) - } - - r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ - Version: 2, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - }), - Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { - return m, nil - }, - }) - if err := r.InternalValidate(nil, true); err != nil { - t.Fatal(err) - } - - // check for missing type - r.StateUpgraders[0].Type = cty.Type{} - if err := r.InternalValidate(nil, true); err == nil { - t.Fatal("StateUpgrader must have type") - } - r.StateUpgraders[0].Type = cty.Object(map[string]cty.Type{ - "id": cty.String, - }) - - // check for missing Upgrade func - r.StateUpgraders[0].Upgrade = nil - if err := r.InternalValidate(nil, true); err == nil { - t.Fatal("StateUpgrader must have an Upgrade func") - } - r.StateUpgraders[0].Upgrade = func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { - return m, nil - } - - // check for skipped version - r.StateUpgraders[0].Version = 0 - r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ - Version: 2, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - }), - Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { - return m, nil - }, - }) - if err := r.InternalValidate(nil, true); err == nil { - t.Fatal("StateUpgraders cannot skip versions") - } - - // add the missing version, but fail because it's still out of order - r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ - Version: 1, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - }), - Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { - return m, nil - }, - }) - if err := r.InternalValidate(nil, true); err == nil { - t.Fatal("upgraders must be defined in order") - } - - r.StateUpgraders[1], r.StateUpgraders[2] = r.StateUpgraders[2], r.StateUpgraders[1] - if err := r.InternalValidate(nil, true); err != nil { - t.Fatal(err) - } - - // can't add an upgrader for a schema >= the current version - r.StateUpgraders = append(r.StateUpgraders, StateUpgrader{ - Version: 3, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - }), - Upgrade: func(m map[string]interface{}, _ interface{}) (map[string]interface{}, error) { - return m, nil - }, - }) - if err := r.InternalValidate(nil, true); err == nil { - t.Fatal("StateUpgraders cannot have a version >= current SchemaVersion") - } -} - -// The legacy provider will need to be able to handle both types of schema -// transformations, which has been retrofitted into the Refresh method. -func TestResource_migrateAndUpgrade(t *testing.T) { - r := &Resource{ - SchemaVersion: 4, - Schema: map[string]*Schema{ - "four": { - Type: TypeInt, - Required: true, - }, - }, - // this MigrateState will take the state to version 2 - MigrateState: func(v int, is *terraform.InstanceState, _ interface{}) (*terraform.InstanceState, error) { - switch v { - case 0: - _, ok := is.Attributes["zero"] - if !ok { - return nil, fmt.Errorf("zero not found in %#v", is.Attributes) - } - is.Attributes["one"] = "1" - delete(is.Attributes, "zero") - fallthrough - case 1: - _, ok := is.Attributes["one"] - if !ok { - return nil, fmt.Errorf("one not found in %#v", is.Attributes) - } - is.Attributes["two"] = "2" - delete(is.Attributes, "one") - default: - return nil, fmt.Errorf("invalid schema version %d", v) - } - return is, nil - }, - } - - r.Read = func(d *ResourceData, m interface{}) error { - return d.Set("four", 4) - } - - r.StateUpgraders = []StateUpgrader{ - { - Version: 2, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - "two": cty.Number, - }), - Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - _, ok := m["two"].(float64) - if !ok { - return nil, fmt.Errorf("two not found in %#v", m) - } - m["three"] = float64(3) - delete(m, "two") - return m, nil - }, - }, - { - Version: 3, - Type: cty.Object(map[string]cty.Type{ - "id": cty.String, - "three": cty.Number, - }), - Upgrade: func(m map[string]interface{}, meta interface{}) (map[string]interface{}, error) { - _, ok := m["three"].(float64) - if !ok { - return nil, fmt.Errorf("three not found in %#v", m) - } - m["four"] = float64(4) - delete(m, "three") - return m, nil - }, - }, - } - - testStates := []*terraform.InstanceState{ - { - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "zero": "0", - }, - Meta: map[string]interface{}{ - "schema_version": "0", - }, - }, - { - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "one": "1", - }, - Meta: map[string]interface{}{ - "schema_version": "1", - }, - }, - { - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "two": "2", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - }, - }, - { - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "three": "3", - }, - Meta: map[string]interface{}{ - "schema_version": "3", - }, - }, - { - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "four": "4", - }, - Meta: map[string]interface{}{ - "schema_version": "4", - }, - }, - } - - for i, s := range testStates { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - newState, err := r.Refresh(s, nil) - if err != nil { - t.Fatal(err) - } - - expected := &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "id": "bar", - "four": "4", - }, - Meta: map[string]interface{}{ - "schema_version": "4", - }, - } - - if !cmp.Equal(expected, newState, equateEmpty) { - t.Fatal(cmp.Diff(expected, newState, equateEmpty)) - } - }) - } -} diff --git a/helper/schema/resource_timeout.go b/helper/schema/resource_timeout.go deleted file mode 100644 index 5ad7aafc8..000000000 --- a/helper/schema/resource_timeout.go +++ /dev/null @@ -1,263 +0,0 @@ -package schema - -import ( - "fmt" - "log" - "time" - - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/copystructure" -) - -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 *terraform.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: %s", 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 delcared 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 *terraform.InstanceDiff) error { - return t.metaEncode(id) -} - -func (t *ResourceTimeout) StateEncode(is *terraform.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 *terraform.InstanceState or -// *terraform.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 *terraform.InstanceDiff: - if instance.Meta == nil { - instance.Meta = make(map[string]interface{}) - } - instance.Meta[TimeoutKey] = m - case *terraform.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 *terraform.InstanceState) error { - return t.metaDecode(id) -} -func (t *ResourceTimeout) DiffDecode(is *terraform.InstanceDiff) error { - return t.metaDecode(is) -} - -func (t *ResourceTimeout) metaDecode(ids interface{}) error { - var rawMeta interface{} - var ok bool - switch rawInstance := ids.(type) { - case *terraform.InstanceDiff: - rawMeta, ok = rawInstance.Meta[TimeoutKey] - if !ok { - return nil - } - case *terraform.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 -} diff --git a/helper/schema/resource_timeout_test.go b/helper/schema/resource_timeout_test.go deleted file mode 100644 index e53bbd849..000000000 --- a/helper/schema/resource_timeout_test.go +++ /dev/null @@ -1,376 +0,0 @@ -package schema - -import ( - "fmt" - "reflect" - "testing" - "time" - - "github.com/hashicorp/terraform/terraform" -) - -func TestResourceTimeout_ConfigDecode_badkey(t *testing.T) { - cases := []struct { - Name string - // what the resource has defined in source - ResourceDefaultTimeout *ResourceTimeout - // configuration provider by user in tf file - Config map[string]interface{} - // what we expect the parsed ResourceTimeout to be - Expected *ResourceTimeout - // Should we have an error (key not defined in source) - ShouldErr bool - }{ - { - Name: "Source does not define 'delete' key", - ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0), - Config: expectedConfigForValues(2, 0, 0, 1, 0), - Expected: timeoutForValues(10, 0, 5, 0, 0), - ShouldErr: true, - }, - { - Name: "Config overrides create", - ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0), - Config: expectedConfigForValues(2, 0, 7, 0, 0), - Expected: timeoutForValues(2, 0, 7, 0, 0), - ShouldErr: false, - }, - { - Name: "Config overrides create, default provided. Should still have zero values", - ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3), - Config: expectedConfigForValues(2, 0, 7, 0, 0), - Expected: timeoutForValues(2, 0, 7, 0, 3), - ShouldErr: false, - }, - { - Name: "Use something besides 'minutes'", - ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3), - Config: map[string]interface{}{ - "create": "2h", - }, - Expected: timeoutForValues(120, 0, 5, 0, 3), - ShouldErr: false, - }, - } - - for i, c := range cases { - t.Run(fmt.Sprintf("%d-%s", i, c.Name), func(t *testing.T) { - r := &Resource{ - Timeouts: c.ResourceDefaultTimeout, - } - - conf := terraform.NewResourceConfigRaw( - map[string]interface{}{ - "foo": "bar", - TimeoutsConfigKey: c.Config, - }, - ) - - timeout := &ResourceTimeout{} - decodeErr := timeout.ConfigDecode(r, conf) - if c.ShouldErr { - if decodeErr == nil { - t.Fatalf("ConfigDecode case (%d): Expected bad timeout key: %s", i, decodeErr) - } - // should error, err was not nil, continue - return - } else { - if decodeErr != nil { - // should not error, error was not nil, fatal - t.Fatalf("decodeError was not nil: %s", decodeErr) - } - } - - if !reflect.DeepEqual(c.Expected, timeout) { - t.Fatalf("ConfigDecode match error case (%d).\nExpected:\n%#v\nGot:\n%#v", i, c.Expected, timeout) - } - }) - } -} - -func TestResourceTimeout_ConfigDecode(t *testing.T) { - r := &Resource{ - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(10 * time.Minute), - Update: DefaultTimeout(5 * time.Minute), - }, - } - - c := terraform.NewResourceConfigRaw( - map[string]interface{}{ - "foo": "bar", - TimeoutsConfigKey: map[string]interface{}{ - "create": "2m", - "update": "1m", - }, - }, - ) - - timeout := &ResourceTimeout{} - err := timeout.ConfigDecode(r, c) - if err != nil { - t.Fatalf("Expected good timeout returned:, %s", err) - } - - expected := &ResourceTimeout{ - Create: DefaultTimeout(2 * time.Minute), - Update: DefaultTimeout(1 * time.Minute), - } - - if !reflect.DeepEqual(timeout, expected) { - t.Fatalf("bad timeout decode.\nExpected:\n%#v\nGot:\n%#v\n", expected, timeout) - } -} - -func TestResourceTimeout_legacyConfigDecode(t *testing.T) { - r := &Resource{ - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(10 * time.Minute), - Update: DefaultTimeout(5 * time.Minute), - }, - } - - c := terraform.NewResourceConfigRaw( - map[string]interface{}{ - "foo": "bar", - TimeoutsConfigKey: []interface{}{ - map[string]interface{}{ - "create": "2m", - "update": "1m", - }, - }, - }, - ) - - timeout := &ResourceTimeout{} - err := timeout.ConfigDecode(r, c) - if err != nil { - t.Fatalf("Expected good timeout returned:, %s", err) - } - - expected := &ResourceTimeout{ - Create: DefaultTimeout(2 * time.Minute), - Update: DefaultTimeout(1 * time.Minute), - } - - if !reflect.DeepEqual(timeout, expected) { - t.Fatalf("bad timeout decode.\nExpected:\n%#v\nGot:\n%#v\n", expected, timeout) - } -} - -func TestResourceTimeout_DiffEncode_basic(t *testing.T) { - cases := []struct { - Timeout *ResourceTimeout - Expected map[string]interface{} - // Not immediately clear when an error would hit - ShouldErr bool - }{ - // Two fields - { - Timeout: timeoutForValues(10, 0, 5, 0, 0), - Expected: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)}, - ShouldErr: false, - }, - // Two fields, one is Default - { - Timeout: timeoutForValues(10, 0, 0, 0, 7), - Expected: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)}, - ShouldErr: false, - }, - // All fields - { - Timeout: timeoutForValues(10, 3, 4, 1, 7), - Expected: map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)}, - ShouldErr: false, - }, - // No fields - { - Timeout: &ResourceTimeout{}, - Expected: nil, - ShouldErr: false, - }, - } - - for _, c := range cases { - state := &terraform.InstanceDiff{} - err := c.Timeout.DiffEncode(state) - if err != nil && !c.ShouldErr { - t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta) - } - - // should maybe just compare [TimeoutKey] but for now we're assuming only - // that in Meta - if !reflect.DeepEqual(state.Meta, c.Expected) { - t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta) - } - } - // same test cases but for InstanceState - for _, c := range cases { - state := &terraform.InstanceState{} - err := c.Timeout.StateEncode(state) - if err != nil && !c.ShouldErr { - t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta) - } - - // should maybe just compare [TimeoutKey] but for now we're assuming only - // that in Meta - if !reflect.DeepEqual(state.Meta, c.Expected) { - t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta) - } - } -} - -func TestResourceTimeout_MetaDecode_basic(t *testing.T) { - cases := []struct { - State *terraform.InstanceDiff - Expected *ResourceTimeout - // Not immediately clear when an error would hit - ShouldErr bool - }{ - // Two fields - { - State: &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)}}, - Expected: timeoutForValues(10, 0, 5, 0, 0), - ShouldErr: false, - }, - // Two fields, one is Default - { - State: &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)}}, - Expected: timeoutForValues(10, 7, 7, 7, 7), - ShouldErr: false, - }, - // All fields - { - State: &terraform.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)}}, - Expected: timeoutForValues(10, 3, 4, 1, 7), - ShouldErr: false, - }, - // No fields - { - State: &terraform.InstanceDiff{}, - Expected: &ResourceTimeout{}, - ShouldErr: false, - }, - } - - for _, c := range cases { - rt := &ResourceTimeout{} - err := rt.DiffDecode(c.State) - if err != nil && !c.ShouldErr { - t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, rt) - } - - // should maybe just compare [TimeoutKey] but for now we're assuming only - // that in Meta - if !reflect.DeepEqual(rt, c.Expected) { - t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, rt) - } - } -} - -func timeoutForValues(create, read, update, del, def int) *ResourceTimeout { - rt := ResourceTimeout{} - - if create != 0 { - rt.Create = DefaultTimeout(time.Duration(create) * time.Minute) - } - if read != 0 { - rt.Read = DefaultTimeout(time.Duration(read) * time.Minute) - } - if update != 0 { - rt.Update = DefaultTimeout(time.Duration(update) * time.Minute) - } - if del != 0 { - rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute) - } - - if def != 0 { - rt.Default = DefaultTimeout(time.Duration(def) * time.Minute) - } - - return &rt -} - -// Generates a ResourceTimeout struct that should reflect the -// d.Timeout("key") results -func expectedTimeoutForValues(create, read, update, del, def int) *ResourceTimeout { - rt := ResourceTimeout{} - - defaultValues := []*int{&create, &read, &update, &del, &def} - for _, v := range defaultValues { - if *v == 0 { - *v = 20 - } - } - - if create != 0 { - rt.Create = DefaultTimeout(time.Duration(create) * time.Minute) - } - if read != 0 { - rt.Read = DefaultTimeout(time.Duration(read) * time.Minute) - } - if update != 0 { - rt.Update = DefaultTimeout(time.Duration(update) * time.Minute) - } - if del != 0 { - rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute) - } - - if def != 0 { - rt.Default = DefaultTimeout(time.Duration(def) * time.Minute) - } - - return &rt -} - -func expectedForValues(create, read, update, del, def int) map[string]interface{} { - ex := make(map[string]interface{}) - - if create != 0 { - ex["create"] = DefaultTimeout(time.Duration(create) * time.Minute).Nanoseconds() - } - if read != 0 { - ex["read"] = DefaultTimeout(time.Duration(read) * time.Minute).Nanoseconds() - } - if update != 0 { - ex["update"] = DefaultTimeout(time.Duration(update) * time.Minute).Nanoseconds() - } - if del != 0 { - ex["delete"] = DefaultTimeout(time.Duration(del) * time.Minute).Nanoseconds() - } - - if def != 0 { - defNano := DefaultTimeout(time.Duration(def) * time.Minute).Nanoseconds() - ex["default"] = defNano - - for _, k := range timeoutKeys() { - if _, ok := ex[k]; !ok { - ex[k] = defNano - } - } - } - - return ex -} - -func expectedConfigForValues(create, read, update, delete, def int) map[string]interface{} { - ex := make(map[string]interface{}, 0) - - if create != 0 { - ex["create"] = fmt.Sprintf("%dm", create) - } - if read != 0 { - ex["read"] = fmt.Sprintf("%dm", read) - } - if update != 0 { - ex["update"] = fmt.Sprintf("%dm", update) - } - if delete != 0 { - ex["delete"] = fmt.Sprintf("%dm", delete) - } - - if def != 0 { - ex["default"] = fmt.Sprintf("%dm", def) - } - return ex -} diff --git a/helper/schema/schema.go b/helper/schema/schema.go deleted file mode 100644 index 089e6b213..000000000 --- a/helper/schema/schema.go +++ /dev/null @@ -1,1854 +0,0 @@ -// schema is a high-level framework for easily writing new providers -// for Terraform. Usage of schema is recommended over attempting to write -// to the low-level plugin interfaces manually. -// -// schema breaks down provider creation into simple CRUD operations for -// resources. The logic of diffing, destroying before creating, updating -// or creating, etc. is all handled by the framework. The plugin author -// only needs to implement a configuration schema and the CRUD operations and -// everything else is meant to just work. -// -// A good starting point is to view the Provider structure. -package schema - -import ( - "context" - "fmt" - "os" - "reflect" - "regexp" - "sort" - "strconv" - "strings" - "sync" - - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/copystructure" - "github.com/mitchellh/mapstructure" -) - -// Name of ENV variable which (if not empty) prefers panic over error -const PanicOnErr = "TF_SCHEMA_PANIC_ON_ERROR" - -// type used for schema package context keys -type contextKey string - -var ( - protoVersionMu sync.Mutex - protoVersion5 = false -) - -func isProto5() bool { - protoVersionMu.Lock() - defer protoVersionMu.Unlock() - return protoVersion5 - -} - -// SetProto5 enables a feature flag for any internal changes required required -// to work with the new plugin protocol. This should not be called by -// provider. -func SetProto5() { - protoVersionMu.Lock() - defer protoVersionMu.Unlock() - protoVersion5 = true -} - -// Schema is used to describe the structure of a value. -// -// Read the documentation of the struct elements for important details. -type Schema struct { - // Type is the type of the value and must be one of the ValueType values. - // - // This type not only determines what type is expected/valid in configuring - // this value, but also what type is returned when ResourceData.Get is - // called. The types returned by Get are: - // - // TypeBool - bool - // TypeInt - int - // TypeFloat - float64 - // TypeString - string - // TypeList - []interface{} - // TypeMap - map[string]interface{} - // TypeSet - *schema.Set - // - Type ValueType - - // ConfigMode allows for overriding the default behaviors for mapping - // schema entries onto configuration constructs. - // - // By default, the Elem field is used to choose whether a particular - // schema is represented in configuration as an attribute or as a nested - // block; if Elem is a *schema.Resource then it's a block and it's an - // attribute otherwise. - // - // If Elem is *schema.Resource then setting ConfigMode to - // SchemaConfigModeAttr will force it to be represented in configuration - // as an attribute, which means that the Computed flag can be used to - // provide default elements when the argument isn't set at all, while still - // allowing the user to force zero elements by explicitly assigning an - // empty list. - // - // When Computed is set without Optional, the attribute is not settable - // in configuration at all and so SchemaConfigModeAttr is the automatic - // behavior, and SchemaConfigModeBlock is not permitted. - ConfigMode SchemaConfigMode - - // If one of these is set, then this item can come from the configuration. - // Both cannot be set. If Optional is set, the value is optional. If - // Required is set, the value is required. - // - // One of these must be set if the value is not computed. That is: - // value either comes from the config, is computed, or is both. - Optional bool - Required bool - - // If this is non-nil, the provided function will be used during diff - // of this field. If this is nil, a default diff for the type of the - // schema will be used. - // - // This allows comparison based on something other than primitive, list - // or map equality - for example SSH public keys may be considered - // equivalent regardless of trailing whitespace. - DiffSuppressFunc SchemaDiffSuppressFunc - - // If this is non-nil, then this will be a default value that is used - // when this item is not set in the configuration. - // - // DefaultFunc can be specified to compute a dynamic default. - // Only one of Default or DefaultFunc can be set. If DefaultFunc is - // used then its return value should be stable to avoid generating - // confusing/perpetual diffs. - // - // Changing either Default or the return value of DefaultFunc can be - // a breaking change, especially if the attribute in question has - // ForceNew set. If a default needs to change to align with changing - // assumptions in an upstream API then it may be necessary to also use - // the MigrateState function on the resource to change the state to match, - // or have the Read function adjust the state value to align with the - // new default. - // - // If Required is true above, then Default cannot be set. DefaultFunc - // can be set with Required. If the DefaultFunc returns nil, then there - // will be no default and the user will be asked to fill it in. - // - // If either of these is set, then the user won't be asked for input - // for this key if the default is not nil. - Default interface{} - DefaultFunc SchemaDefaultFunc - - // Description is used as the description for docs or asking for user - // input. It should be relatively short (a few sentences max) and should - // be formatted to fit a CLI. - Description string - - // InputDefault is the default value to use for when inputs are requested. - // This differs from Default in that if Default is set, no input is - // asked for. If Input is asked, this will be the default value offered. - InputDefault string - - // The fields below relate to diffs. - // - // If Computed is true, then the result of this value is computed - // (unless specified by config) on creation. - // - // If ForceNew is true, then a change in this resource necessitates - // the creation of a new resource. - // - // StateFunc is a function called to change the value of this before - // storing it in the state (and likewise before comparing for diffs). - // The use for this is for example with large strings, you may want - // to simply store the hash of it. - Computed bool - ForceNew bool - StateFunc SchemaStateFunc - - // The following fields are only set for a TypeList, TypeSet, or TypeMap. - // - // Elem represents the element type. For a TypeMap, it must be a *Schema - // with a Type that is one of the primitives: TypeString, TypeBool, - // TypeInt, or TypeFloat. Otherwise it may be either a *Schema or a - // *Resource. If it is *Schema, the element type is just a simple value. - // If it is *Resource, the element type is a complex structure, - // potentially managed via its own CRUD actions on the API. - Elem interface{} - - // The following fields are only set for a TypeList or TypeSet. - // - // MaxItems defines a maximum amount of items that can exist within a - // TypeSet or TypeList. Specific use cases would be if a TypeSet is being - // used to wrap a complex structure, however more than one instance would - // cause instability. - // - // MinItems defines a minimum amount of items that can exist within a - // TypeSet or TypeList. Specific use cases would be if a TypeSet is being - // used to wrap a complex structure, however less than one instance would - // cause instability. - // - // If the field Optional is set to true then MinItems is ignored and thus - // effectively zero. - MaxItems int - MinItems int - - // PromoteSingle originally allowed for a single element to be assigned - // where a primitive list was expected, but this no longer works from - // Terraform v0.12 onwards (Terraform Core will require a list to be set - // regardless of what this is set to) and so only applies to Terraform v0.11 - // and earlier, and so should be used only to retain this functionality - // for those still using v0.11 with a provider that formerly used this. - PromoteSingle bool - - // The following fields are only valid for a TypeSet type. - // - // Set defines a function to determine the unique ID of an item so that - // a proper set can be built. - Set SchemaSetFunc - - // ComputedWhen is a set of queries on the configuration. Whenever any - // of these things is changed, it will require a recompute (this requires - // that Computed is set to true). - // - // NOTE: This currently does not work. - ComputedWhen []string - - // ConflictsWith is a set of schema keys that conflict with this schema. - // This will only check that they're set in the _config_. This will not - // raise an error for a malfunctioning resource that sets a conflicting - // key. - ConflictsWith []string - - // When Deprecated is set, this attribute is deprecated. - // - // A deprecated field still works, but will probably stop working in near - // future. This string is the message shown to the user with instructions on - // how to address the deprecation. - Deprecated string - - // When Removed is set, this attribute has been removed from the schema - // - // Removed attributes can be left in the Schema to generate informative error - // messages for the user when they show up in resource configurations. - // This string is the message shown to the user with instructions on - // what do to about the removed attribute. - Removed string - - // ValidateFunc allows individual fields to define arbitrary validation - // logic. It is yielded the provided config value as an interface{} that is - // guaranteed to be of the proper Schema type, and it can yield warnings or - // errors based on inspection of that value. - // - // ValidateFunc is honored only when the schema's Type is set to TypeInt, - // TypeFloat, TypeString, TypeBool, or TypeMap. It is ignored for all other types. - ValidateFunc SchemaValidateFunc - - // Sensitive ensures that the attribute's value does not get displayed in - // logs or regular output. It should be used for passwords or other - // secret fields. Future versions of Terraform may encrypt these - // values. - Sensitive bool -} - -// SchemaConfigMode is used to influence how a schema item is mapped into a -// corresponding configuration construct, using the ConfigMode field of -// Schema. -type SchemaConfigMode int - -const ( - SchemaConfigModeAuto SchemaConfigMode = iota - SchemaConfigModeAttr - SchemaConfigModeBlock -) - -// SchemaDiffSuppressFunc is a function which can be used to determine -// whether a detected diff on a schema element is "valid" or not, and -// suppress it from the plan if necessary. -// -// Return true if the diff should be suppressed, false to retain it. -type SchemaDiffSuppressFunc func(k, old, new string, d *ResourceData) bool - -// SchemaDefaultFunc is a function called to return a default value for -// a field. -type SchemaDefaultFunc func() (interface{}, error) - -// EnvDefaultFunc is a helper function that returns the value of the -// given environment variable, if one exists, or the default value -// otherwise. -func EnvDefaultFunc(k string, dv interface{}) SchemaDefaultFunc { - return func() (interface{}, error) { - if v := os.Getenv(k); v != "" { - return v, nil - } - - return dv, nil - } -} - -// MultiEnvDefaultFunc is a helper function that returns the value of the first -// environment variable in the given list that returns a non-empty value. If -// none of the environment variables return a value, the default value is -// returned. -func MultiEnvDefaultFunc(ks []string, dv interface{}) SchemaDefaultFunc { - return func() (interface{}, error) { - for _, k := range ks { - if v := os.Getenv(k); v != "" { - return v, nil - } - } - return dv, nil - } -} - -// SchemaSetFunc is a function that must return a unique ID for the given -// element. This unique ID is used to store the element in a hash. -type SchemaSetFunc func(interface{}) int - -// SchemaStateFunc is a function used to convert some type to a string -// to be stored in the state. -type SchemaStateFunc func(interface{}) string - -// SchemaValidateFunc is a function used to validate a single field in the -// schema. -type SchemaValidateFunc func(interface{}, string) ([]string, []error) - -func (s *Schema) GoString() string { - return fmt.Sprintf("*%#v", *s) -} - -// Returns a default value for this schema by either reading Default or -// evaluating DefaultFunc. If neither of these are defined, returns nil. -func (s *Schema) DefaultValue() (interface{}, error) { - if s.Default != nil { - return s.Default, nil - } - - if s.DefaultFunc != nil { - defaultValue, err := s.DefaultFunc() - if err != nil { - return nil, fmt.Errorf("error loading default: %s", err) - } - return defaultValue, nil - } - - return nil, nil -} - -// Returns a zero value for the schema. -func (s *Schema) ZeroValue() interface{} { - // If it's a set then we'll do a bit of extra work to provide the - // right hashing function in our empty value. - if s.Type == TypeSet { - setFunc := s.Set - if setFunc == nil { - // Default set function uses the schema to hash the whole value - elem := s.Elem - switch t := elem.(type) { - case *Schema: - setFunc = HashSchema(t) - case *Resource: - setFunc = HashResource(t) - default: - panic("invalid set element type") - } - } - return &Set{F: setFunc} - } else { - return s.Type.Zero() - } -} - -func (s *Schema) finalizeDiff(d *terraform.ResourceAttrDiff, customized bool) *terraform.ResourceAttrDiff { - if d == nil { - return d - } - - if s.Type == TypeBool { - normalizeBoolString := func(s string) string { - switch s { - case "0": - return "false" - case "1": - return "true" - } - return s - } - d.Old = normalizeBoolString(d.Old) - d.New = normalizeBoolString(d.New) - } - - if s.Computed && !d.NewRemoved && d.New == "" { - // Computed attribute without a new value set - d.NewComputed = true - } - - if s.ForceNew { - // ForceNew, mark that this field is requiring new under the - // following conditions, explained below: - // - // * Old != New - There is a change in value. This field - // is therefore causing a new resource. - // - // * NewComputed - This field is being computed, hence a - // potential change in value, mark as causing a new resource. - d.RequiresNew = d.Old != d.New || d.NewComputed - } - - if d.NewRemoved { - return d - } - - if s.Computed { - // FIXME: This is where the customized bool from getChange finally - // comes into play. It allows the previously incorrect behavior - // of an empty string being used as "unset" when the value is - // computed. This should be removed once we can properly - // represent an unset/nil value from the configuration. - if !customized { - if d.Old != "" && d.New == "" { - // This is a computed value with an old value set already, - // just let it go. - return nil - } - } - - if d.New == "" && !d.NewComputed { - // Computed attribute without a new value set - d.NewComputed = true - } - } - - if s.Sensitive { - // Set the Sensitive flag so output is hidden in the UI - d.Sensitive = true - } - - return d -} - -// InternalMap is used to aid in the transition to the new schema types and -// protocol. The name is not meant to convey any usefulness, as this is not to -// be used directly by any providers. -type InternalMap = schemaMap - -// schemaMap is a wrapper that adds nice functions on top of schemas. -type schemaMap map[string]*Schema - -func (m schemaMap) panicOnError() bool { - if os.Getenv(PanicOnErr) != "" { - return true - } - return false -} - -// Data returns a ResourceData for the given schema, state, and diff. -// -// The diff is optional. -func (m schemaMap) Data( - s *terraform.InstanceState, - d *terraform.InstanceDiff) (*ResourceData, error) { - return &ResourceData{ - schema: m, - state: s, - diff: d, - panicOnError: m.panicOnError(), - }, nil -} - -// DeepCopy returns a copy of this schemaMap. The copy can be safely modified -// without affecting the original. -func (m *schemaMap) DeepCopy() schemaMap { - copy, err := copystructure.Config{Lock: true}.Copy(m) - if err != nil { - panic(err) - } - return *copy.(*schemaMap) -} - -// Diff returns the diff for a resource given the schema map, -// state, and configuration. -func (m schemaMap) Diff( - s *terraform.InstanceState, - c *terraform.ResourceConfig, - customizeDiff CustomizeDiffFunc, - meta interface{}, - handleRequiresNew bool) (*terraform.InstanceDiff, error) { - result := new(terraform.InstanceDiff) - result.Attributes = make(map[string]*terraform.ResourceAttrDiff) - - // Make sure to mark if the resource is tainted - if s != nil { - result.DestroyTainted = s.Tainted - } - - d := &ResourceData{ - schema: m, - state: s, - config: c, - panicOnError: m.panicOnError(), - } - - for k, schema := range m { - err := m.diff(k, schema, result, d, false) - if err != nil { - return nil, err - } - } - - // Remove any nil diffs just to keep things clean - for k, v := range result.Attributes { - if v == nil { - delete(result.Attributes, k) - } - } - - // If this is a non-destroy diff, call any custom diff logic that has been - // defined. - if !result.DestroyTainted && customizeDiff != nil { - mc := m.DeepCopy() - rd := newResourceDiff(mc, c, s, result) - if err := customizeDiff(rd, meta); err != nil { - return nil, err - } - for _, k := range rd.UpdatedKeys() { - err := m.diff(k, mc[k], result, rd, false) - if err != nil { - return nil, err - } - } - } - - if handleRequiresNew { - // If the diff requires a new resource, then we recompute the diff - // so we have the complete new resource diff, and preserve the - // RequiresNew fields where necessary so the user knows exactly what - // caused that. - if result.RequiresNew() { - // Create the new diff - result2 := new(terraform.InstanceDiff) - result2.Attributes = make(map[string]*terraform.ResourceAttrDiff) - - // Preserve the DestroyTainted flag - result2.DestroyTainted = result.DestroyTainted - - // Reset the data to not contain state. We have to call init() - // again in order to reset the FieldReaders. - d.state = nil - d.init() - - // Perform the diff again - for k, schema := range m { - err := m.diff(k, schema, result2, d, false) - if err != nil { - return nil, err - } - } - - // Re-run customization - if !result2.DestroyTainted && customizeDiff != nil { - mc := m.DeepCopy() - rd := newResourceDiff(mc, c, d.state, result2) - if err := customizeDiff(rd, meta); err != nil { - return nil, err - } - for _, k := range rd.UpdatedKeys() { - err := m.diff(k, mc[k], result2, rd, false) - if err != nil { - return nil, err - } - } - } - - // Force all the fields to not force a new since we know what we - // want to force new. - for k, attr := range result2.Attributes { - if attr == nil { - continue - } - - if attr.RequiresNew { - attr.RequiresNew = false - } - - if s != nil { - attr.Old = s.Attributes[k] - } - } - - // Now copy in all the requires new diffs... - for k, attr := range result.Attributes { - if attr == nil { - continue - } - - newAttr, ok := result2.Attributes[k] - if !ok { - newAttr = attr - } - - if attr.RequiresNew { - newAttr.RequiresNew = true - } - - result2.Attributes[k] = newAttr - } - - // And set the diff! - result = result2 - } - - } - - // Go through and detect all of the ComputedWhens now that we've - // finished the diff. - // TODO - - if result.Empty() { - // If we don't have any diff elements, just return nil - return nil, nil - } - - return result, nil -} - -// Input implements the terraform.ResourceProvider method by asking -// for input for required configuration keys that don't have a value. -func (m schemaMap) Input( - input terraform.UIInput, - c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - keys := make([]string, 0, len(m)) - for k, _ := range m { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, k := range keys { - v := m[k] - - // Skip things that don't require config, if that is even valid - // for a provider schema. - // Required XOR Optional must always be true to validate, so we only - // need to check one. - if v.Optional { - continue - } - - // Deprecated fields should never prompt - if v.Deprecated != "" { - continue - } - - // Skip things that have a value of some sort already - if _, ok := c.Raw[k]; ok { - continue - } - - // Skip if it has a default value - defaultValue, err := v.DefaultValue() - if err != nil { - return nil, fmt.Errorf("%s: error loading default: %s", k, err) - } - if defaultValue != nil { - continue - } - - var value interface{} - switch v.Type { - case TypeBool, TypeInt, TypeFloat, TypeSet, TypeList: - continue - case TypeString: - value, err = m.inputString(input, k, v) - default: - panic(fmt.Sprintf("Unknown type for input: %#v", v.Type)) - } - - if err != nil { - return nil, fmt.Errorf( - "%s: %s", k, err) - } - - c.Config[k] = value - } - - return c, nil -} - -// Validate validates the configuration against this schema mapping. -func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) { - return m.validateObject("", m, c) -} - -// InternalValidate validates the format of this schema. This should be called -// from a unit test (and not in user-path code) to verify that a schema -// is properly built. -func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error { - return m.internalValidate(topSchemaMap, false) -} - -func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) error { - if topSchemaMap == nil { - topSchemaMap = m - } - for k, v := range m { - if v.Type == TypeInvalid { - return fmt.Errorf("%s: Type must be specified", k) - } - - if v.Optional && v.Required { - return fmt.Errorf("%s: Optional or Required must be set, not both", k) - } - - if v.Required && v.Computed { - return fmt.Errorf("%s: Cannot be both Required and Computed", k) - } - - if !v.Required && !v.Optional && !v.Computed { - return fmt.Errorf("%s: One of optional, required, or computed must be set", k) - } - - computedOnly := v.Computed && !v.Optional - - switch v.ConfigMode { - case SchemaConfigModeBlock: - if _, ok := v.Elem.(*Resource); !ok { - return fmt.Errorf("%s: ConfigMode of block is allowed only when Elem is *schema.Resource", k) - } - if attrsOnly { - return fmt.Errorf("%s: ConfigMode of block cannot be used in child of schema with ConfigMode of attribute", k) - } - if computedOnly { - return fmt.Errorf("%s: ConfigMode of block cannot be used for computed schema", k) - } - case SchemaConfigModeAttr: - // anything goes - case SchemaConfigModeAuto: - // Since "Auto" for Elem: *Resource would create a nested block, - // and that's impossible inside an attribute, we require it to be - // explicitly overridden as mode "Attr" for clarity. - if _, ok := v.Elem.(*Resource); ok { - if attrsOnly { - return fmt.Errorf("%s: in *schema.Resource with ConfigMode of attribute, so must also have ConfigMode of attribute", k) - } - } - default: - return fmt.Errorf("%s: invalid ConfigMode value", k) - } - - if v.Computed && v.Default != nil { - return fmt.Errorf("%s: Default must be nil if computed", k) - } - - if v.Required && v.Default != nil { - return fmt.Errorf("%s: Default cannot be set with Required", k) - } - - if len(v.ComputedWhen) > 0 && !v.Computed { - return fmt.Errorf("%s: ComputedWhen can only be set with Computed", k) - } - - if len(v.ConflictsWith) > 0 && v.Required { - return fmt.Errorf("%s: ConflictsWith cannot be set with Required", k) - } - - if len(v.ConflictsWith) > 0 { - for _, key := range v.ConflictsWith { - parts := strings.Split(key, ".") - sm := topSchemaMap - var target *Schema - for _, part := range parts { - // Skip index fields - if _, err := strconv.Atoi(part); err == nil { - continue - } - - var ok bool - if target, ok = sm[part]; !ok { - return fmt.Errorf("%s: ConflictsWith references unknown attribute (%s) at part (%s)", k, key, part) - } - - if subResource, ok := target.Elem.(*Resource); ok { - sm = schemaMap(subResource.Schema) - } - } - if target == nil { - return fmt.Errorf("%s: ConflictsWith cannot find target attribute (%s), sm: %#v", k, key, sm) - } - if target.Required { - return fmt.Errorf("%s: ConflictsWith cannot contain Required attribute (%s)", k, key) - } - - if len(target.ComputedWhen) > 0 { - return fmt.Errorf("%s: ConflictsWith cannot contain Computed(When) attribute (%s)", k, key) - } - } - } - - if v.Type == TypeList || v.Type == TypeSet { - if v.Elem == nil { - return fmt.Errorf("%s: Elem must be set for lists", k) - } - - if v.Default != nil { - return fmt.Errorf("%s: Default is not valid for lists or sets", k) - } - - if v.Type != TypeSet && v.Set != nil { - return fmt.Errorf("%s: Set can only be set for TypeSet", k) - } - - switch t := v.Elem.(type) { - case *Resource: - attrsOnly := attrsOnly || v.ConfigMode == SchemaConfigModeAttr - - if err := schemaMap(t.Schema).internalValidate(topSchemaMap, attrsOnly); err != nil { - return err - } - case *Schema: - bad := t.Computed || t.Optional || t.Required - if bad { - return fmt.Errorf( - "%s: Elem must have only Type set", k) - } - } - } else { - if v.MaxItems > 0 || v.MinItems > 0 { - return fmt.Errorf("%s: MaxItems and MinItems are only supported on lists or sets", k) - } - } - - // Computed-only field - if v.Computed && !v.Optional { - if v.ValidateFunc != nil { - return fmt.Errorf("%s: ValidateFunc is for validating user input, "+ - "there's nothing to validate on computed-only field", k) - } - if v.DiffSuppressFunc != nil { - return fmt.Errorf("%s: DiffSuppressFunc is for suppressing differences"+ - " between config and state representation. "+ - "There is no config for computed-only field, nothing to compare.", k) - } - } - - if v.ValidateFunc != nil { - switch v.Type { - case TypeList, TypeSet: - return fmt.Errorf("%s: ValidateFunc is not yet supported on lists or sets.", k) - } - } - - if v.Deprecated == "" && v.Removed == "" { - if !isValidFieldName(k) { - return fmt.Errorf("%s: Field name may only contain lowercase alphanumeric characters & underscores.", k) - } - } - } - - return nil -} - -func isValidFieldName(name string) bool { - re := regexp.MustCompile("^[a-z0-9_]+$") - return re.MatchString(name) -} - -// resourceDiffer is an interface that is used by the private diff functions. -// This helps facilitate diff logic for both ResourceData and ResoureDiff with -// minimal divergence in code. -type resourceDiffer interface { - diffChange(string) (interface{}, interface{}, bool, bool, bool) - Get(string) interface{} - GetChange(string) (interface{}, interface{}) - GetOk(string) (interface{}, bool) - HasChange(string) bool - Id() string -} - -func (m schemaMap) diff( - k string, - schema *Schema, - diff *terraform.InstanceDiff, - d resourceDiffer, - all bool) error { - - unsupressedDiff := new(terraform.InstanceDiff) - unsupressedDiff.Attributes = make(map[string]*terraform.ResourceAttrDiff) - - var err error - switch schema.Type { - case TypeBool, TypeInt, TypeFloat, TypeString: - err = m.diffString(k, schema, unsupressedDiff, d, all) - case TypeList: - err = m.diffList(k, schema, unsupressedDiff, d, all) - case TypeMap: - err = m.diffMap(k, schema, unsupressedDiff, d, all) - case TypeSet: - err = m.diffSet(k, schema, unsupressedDiff, d, all) - default: - err = fmt.Errorf("%s: unknown type %#v", k, schema.Type) - } - - for attrK, attrV := range unsupressedDiff.Attributes { - switch rd := d.(type) { - case *ResourceData: - if schema.DiffSuppressFunc != nil && attrV != nil && - schema.DiffSuppressFunc(attrK, attrV.Old, attrV.New, rd) { - // If this attr diff is suppressed, we may still need it in the - // overall diff if it's contained within a set. Rather than - // dropping the diff, make it a NOOP. - if !all { - continue - } - - attrV = &terraform.ResourceAttrDiff{ - Old: attrV.Old, - New: attrV.Old, - } - } - } - diff.Attributes[attrK] = attrV - } - - return err -} - -func (m schemaMap) diffList( - k string, - schema *Schema, - diff *terraform.InstanceDiff, - d resourceDiffer, - all bool) error { - o, n, _, computedList, customized := d.diffChange(k) - if computedList { - n = nil - } - nSet := n != nil - - // If we have an old value and no new value is set or will be - // computed once all variables can be interpolated and we're - // computed, then nothing has changed. - if o != nil && n == nil && !computedList && schema.Computed { - return nil - } - - if o == nil { - o = []interface{}{} - } - if n == nil { - n = []interface{}{} - } - if s, ok := o.(*Set); ok { - o = s.List() - } - if s, ok := n.(*Set); ok { - n = s.List() - } - os := o.([]interface{}) - vs := n.([]interface{}) - - // If the new value was set, and the two are equal, then we're done. - // We have to do this check here because sets might be NOT - // reflect.DeepEqual so we need to wait until we get the []interface{} - if !all && nSet && reflect.DeepEqual(os, vs) { - return nil - } - - // Get the counts - oldLen := len(os) - newLen := len(vs) - oldStr := strconv.FormatInt(int64(oldLen), 10) - - // If the whole list is computed, then say that the # is computed - if computedList { - diff.Attributes[k+".#"] = &terraform.ResourceAttrDiff{ - Old: oldStr, - NewComputed: true, - RequiresNew: schema.ForceNew, - } - return nil - } - - // If the counts are not the same, then record that diff - changed := oldLen != newLen - computed := oldLen == 0 && newLen == 0 && schema.Computed - if changed || computed || all { - countSchema := &Schema{ - Type: TypeInt, - Computed: schema.Computed, - ForceNew: schema.ForceNew, - } - - newStr := "" - if !computed { - newStr = strconv.FormatInt(int64(newLen), 10) - } else { - oldStr = "" - } - - diff.Attributes[k+".#"] = countSchema.finalizeDiff( - &terraform.ResourceAttrDiff{ - Old: oldStr, - New: newStr, - }, - customized, - ) - } - - // Figure out the maximum - maxLen := oldLen - if newLen > maxLen { - maxLen = newLen - } - - switch t := schema.Elem.(type) { - case *Resource: - // This is a complex resource - for i := 0; i < maxLen; i++ { - for k2, schema := range t.Schema { - subK := fmt.Sprintf("%s.%d.%s", k, i, k2) - err := m.diff(subK, schema, diff, d, all) - if err != nil { - return err - } - } - } - case *Schema: - // Copy the schema so that we can set Computed/ForceNew from - // the parent schema (the TypeList). - t2 := *t - t2.ForceNew = schema.ForceNew - - // This is just a primitive element, so go through each and - // just diff each. - for i := 0; i < maxLen; i++ { - subK := fmt.Sprintf("%s.%d", k, i) - err := m.diff(subK, &t2, diff, d, all) - if err != nil { - return err - } - } - default: - return fmt.Errorf("%s: unknown element type (internal)", k) - } - - return nil -} - -func (m schemaMap) diffMap( - k string, - schema *Schema, - diff *terraform.InstanceDiff, - d resourceDiffer, - all bool) error { - prefix := k + "." - - // First get all the values from the state - var stateMap, configMap map[string]string - o, n, _, nComputed, customized := d.diffChange(k) - if err := mapstructure.WeakDecode(o, &stateMap); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - if err := mapstructure.WeakDecode(n, &configMap); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - // Keep track of whether the state _exists_ at all prior to clearing it - stateExists := o != nil - - // Delete any count values, since we don't use those - delete(configMap, "%") - delete(stateMap, "%") - - // Check if the number of elements has changed. - oldLen, newLen := len(stateMap), len(configMap) - changed := oldLen != newLen - if oldLen != 0 && newLen == 0 && schema.Computed { - changed = false - } - - // It is computed if we have no old value, no new value, the schema - // says it is computed, and it didn't exist in the state before. The - // last point means: if it existed in the state, even empty, then it - // has already been computed. - computed := oldLen == 0 && newLen == 0 && schema.Computed && !stateExists - - // If the count has changed or we're computed, then add a diff for the - // count. "nComputed" means that the new value _contains_ a value that - // is computed. We don't do granular diffs for this yet, so we mark the - // whole map as computed. - if changed || computed || nComputed { - countSchema := &Schema{ - Type: TypeInt, - Computed: schema.Computed || nComputed, - ForceNew: schema.ForceNew, - } - - oldStr := strconv.FormatInt(int64(oldLen), 10) - newStr := "" - if !computed && !nComputed { - newStr = strconv.FormatInt(int64(newLen), 10) - } else { - oldStr = "" - } - - diff.Attributes[k+".%"] = countSchema.finalizeDiff( - &terraform.ResourceAttrDiff{ - Old: oldStr, - New: newStr, - }, - customized, - ) - } - - // If the new map is nil and we're computed, then ignore it. - if n == nil && schema.Computed { - return nil - } - - // Now we compare, preferring values from the config map - for k, v := range configMap { - old, ok := stateMap[k] - delete(stateMap, k) - - if old == v && ok && !all { - continue - } - - diff.Attributes[prefix+k] = schema.finalizeDiff( - &terraform.ResourceAttrDiff{ - Old: old, - New: v, - }, - customized, - ) - } - for k, v := range stateMap { - diff.Attributes[prefix+k] = schema.finalizeDiff( - &terraform.ResourceAttrDiff{ - Old: v, - NewRemoved: true, - }, - customized, - ) - } - - return nil -} - -func (m schemaMap) diffSet( - k string, - schema *Schema, - diff *terraform.InstanceDiff, - d resourceDiffer, - all bool) error { - - o, n, _, computedSet, customized := d.diffChange(k) - if computedSet { - n = nil - } - nSet := n != nil - - // If we have an old value and no new value is set or will be - // computed once all variables can be interpolated and we're - // computed, then nothing has changed. - if o != nil && n == nil && !computedSet && schema.Computed { - return nil - } - - if o == nil { - o = schema.ZeroValue().(*Set) - } - if n == nil { - n = schema.ZeroValue().(*Set) - } - os := o.(*Set) - ns := n.(*Set) - - // If the new value was set, compare the listCode's to determine if - // the two are equal. Comparing listCode's instead of the actual values - // is needed because there could be computed values in the set which - // would result in false positives while comparing. - if !all && nSet && reflect.DeepEqual(os.listCode(), ns.listCode()) { - return nil - } - - // Get the counts - oldLen := os.Len() - newLen := ns.Len() - oldStr := strconv.Itoa(oldLen) - newStr := strconv.Itoa(newLen) - - // Build a schema for our count - countSchema := &Schema{ - Type: TypeInt, - Computed: schema.Computed, - ForceNew: schema.ForceNew, - } - - // If the set computed then say that the # is computed - if computedSet || schema.Computed && !nSet { - // If # already exists, equals 0 and no new set is supplied, there - // is nothing to record in the diff - count, ok := d.GetOk(k + ".#") - if ok && count.(int) == 0 && !nSet && !computedSet { - return nil - } - - // Set the count but make sure that if # does not exist, we don't - // use the zeroed value - countStr := strconv.Itoa(count.(int)) - if !ok { - countStr = "" - } - - diff.Attributes[k+".#"] = countSchema.finalizeDiff( - &terraform.ResourceAttrDiff{ - Old: countStr, - NewComputed: true, - }, - customized, - ) - return nil - } - - // If the counts are not the same, then record that diff - changed := oldLen != newLen - if changed || all { - diff.Attributes[k+".#"] = countSchema.finalizeDiff( - &terraform.ResourceAttrDiff{ - Old: oldStr, - New: newStr, - }, - customized, - ) - } - - // Build the list of codes that will make up our set. This is the - // removed codes as well as all the codes in the new codes. - codes := make([][]string, 2) - codes[0] = os.Difference(ns).listCode() - codes[1] = ns.listCode() - for _, list := range codes { - for _, code := range list { - switch t := schema.Elem.(type) { - case *Resource: - // This is a complex resource - for k2, schema := range t.Schema { - subK := fmt.Sprintf("%s.%s.%s", k, code, k2) - err := m.diff(subK, schema, diff, d, true) - if err != nil { - return err - } - } - case *Schema: - // Copy the schema so that we can set Computed/ForceNew from - // the parent schema (the TypeSet). - t2 := *t - t2.ForceNew = schema.ForceNew - - // This is just a primitive element, so go through each and - // just diff each. - subK := fmt.Sprintf("%s.%s", k, code) - err := m.diff(subK, &t2, diff, d, true) - if err != nil { - return err - } - default: - return fmt.Errorf("%s: unknown element type (internal)", k) - } - } - } - - return nil -} - -func (m schemaMap) diffString( - k string, - schema *Schema, - diff *terraform.InstanceDiff, - d resourceDiffer, - all bool) error { - var originalN interface{} - var os, ns string - o, n, _, computed, customized := d.diffChange(k) - if schema.StateFunc != nil && n != nil { - originalN = n - n = schema.StateFunc(n) - } - nraw := n - if nraw == nil && o != nil { - nraw = schema.Type.Zero() - } - if err := mapstructure.WeakDecode(o, &os); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - if err := mapstructure.WeakDecode(nraw, &ns); err != nil { - return fmt.Errorf("%s: %s", k, err) - } - - if os == ns && !all && !computed { - // They're the same value. If there old value is not blank or we - // have an ID, then return right away since we're already setup. - if os != "" || d.Id() != "" { - return nil - } - - // Otherwise, only continue if we're computed - if !schema.Computed { - return nil - } - } - - removed := false - if o != nil && n == nil && !computed { - removed = true - } - if removed && schema.Computed { - return nil - } - - diff.Attributes[k] = schema.finalizeDiff( - &terraform.ResourceAttrDiff{ - Old: os, - New: ns, - NewExtra: originalN, - NewRemoved: removed, - NewComputed: computed, - }, - customized, - ) - - return nil -} - -func (m schemaMap) inputString( - input terraform.UIInput, - k string, - schema *Schema) (interface{}, error) { - result, err := input.Input(context.Background(), &terraform.InputOpts{ - Id: k, - Query: k, - Description: schema.Description, - Default: schema.InputDefault, - }) - - return result, err -} - -func (m schemaMap) validate( - k string, - schema *Schema, - c *terraform.ResourceConfig) ([]string, []error) { - raw, ok := c.Get(k) - if !ok && schema.DefaultFunc != nil { - // We have a dynamic default. Check if we have a value. - var err error - raw, err = schema.DefaultFunc() - if err != nil { - return nil, []error{fmt.Errorf( - "%q, error loading default: %s", k, err)} - } - - // We're okay as long as we had a value set - ok = raw != nil - } - if !ok { - if schema.Required { - return nil, []error{fmt.Errorf( - "%q: required field is not set", k)} - } - - return nil, nil - } - - if !schema.Required && !schema.Optional { - // This is a computed-only field - return nil, []error{fmt.Errorf( - "%q: this field cannot be set", k)} - } - - // If the value is unknown then we can't validate it yet. - // In particular, this avoids spurious type errors where downstream - // validation code sees UnknownVariableValue as being just a string. - // The SDK has to allow the unknown value through initially, so that - // Required fields set via an interpolated value are accepted. - if !isWhollyKnown(raw) { - if schema.Deprecated != "" { - return []string{fmt.Sprintf("%q: [DEPRECATED] %s", k, schema.Deprecated)}, nil - } - return nil, nil - } - - err := m.validateConflictingAttributes(k, schema, c) - if err != nil { - return nil, []error{err} - } - - return m.validateType(k, raw, schema, c) -} - -// isWhollyKnown returns false if the argument contains an UnknownVariableValue -func isWhollyKnown(raw interface{}) bool { - switch raw := raw.(type) { - case string: - if raw == hcl2shim.UnknownVariableValue { - return false - } - case []interface{}: - for _, v := range raw { - if !isWhollyKnown(v) { - return false - } - } - case map[string]interface{}: - for _, v := range raw { - if !isWhollyKnown(v) { - return false - } - } - } - return true -} -func (m schemaMap) validateConflictingAttributes( - k string, - schema *Schema, - c *terraform.ResourceConfig) error { - - if len(schema.ConflictsWith) == 0 { - return nil - } - - for _, conflictingKey := range schema.ConflictsWith { - if raw, ok := c.Get(conflictingKey); ok { - if raw == hcl2shim.UnknownVariableValue { - // An unknown value might become unset (null) once known, so - // we must defer validation until it's known. - continue - } - return fmt.Errorf( - "%q: conflicts with %s", k, conflictingKey) - } - } - - return nil -} - -func (m schemaMap) validateList( - k string, - raw interface{}, - schema *Schema, - c *terraform.ResourceConfig) ([]string, []error) { - // first check if the list is wholly unknown - if s, ok := raw.(string); ok { - if s == hcl2shim.UnknownVariableValue { - return nil, nil - } - } - - // schemaMap can't validate nil - if raw == nil { - return nil, nil - } - - // We use reflection to verify the slice because you can't - // case to []interface{} unless the slice is exactly that type. - rawV := reflect.ValueOf(raw) - - // If we support promotion and the raw value isn't a slice, wrap - // it in []interface{} and check again. - if schema.PromoteSingle && rawV.Kind() != reflect.Slice { - raw = []interface{}{raw} - rawV = reflect.ValueOf(raw) - } - - if rawV.Kind() != reflect.Slice { - return nil, []error{fmt.Errorf( - "%s: should be a list", k)} - } - - // We can't validate list length if this came from a dynamic block. - // Since there's no way to determine if something was from a dynamic block - // at this point, we're going to skip validation in the new protocol if - // there are any unknowns. Validate will eventually be called again once - // all values are known. - if isProto5() && !isWhollyKnown(raw) { - return nil, nil - } - - // Validate length - if schema.MaxItems > 0 && rawV.Len() > schema.MaxItems { - return nil, []error{fmt.Errorf( - "%s: attribute supports %d item maximum, config has %d declared", k, schema.MaxItems, rawV.Len())} - } - - if schema.MinItems > 0 && rawV.Len() < schema.MinItems { - return nil, []error{fmt.Errorf( - "%s: attribute supports %d item as a minimum, config has %d declared", k, schema.MinItems, rawV.Len())} - } - - // Now build the []interface{} - raws := make([]interface{}, rawV.Len()) - for i, _ := range raws { - raws[i] = rawV.Index(i).Interface() - } - - var ws []string - var es []error - for i, raw := range raws { - key := fmt.Sprintf("%s.%d", k, i) - - // Reify the key value from the ResourceConfig. - // If the list was computed we have all raw values, but some of these - // may be known in the config, and aren't individually marked as Computed. - if r, ok := c.Get(key); ok { - raw = r - } - - var ws2 []string - var es2 []error - switch t := schema.Elem.(type) { - case *Resource: - // This is a sub-resource - ws2, es2 = m.validateObject(key, t.Schema, c) - case *Schema: - ws2, es2 = m.validateType(key, raw, t, c) - } - - if len(ws2) > 0 { - ws = append(ws, ws2...) - } - if len(es2) > 0 { - es = append(es, es2...) - } - } - - return ws, es -} - -func (m schemaMap) validateMap( - k string, - raw interface{}, - schema *Schema, - c *terraform.ResourceConfig) ([]string, []error) { - // first check if the list is wholly unknown - if s, ok := raw.(string); ok { - if s == hcl2shim.UnknownVariableValue { - return nil, nil - } - } - - // schemaMap can't validate nil - if raw == nil { - return nil, nil - } - // We use reflection to verify the slice because you can't - // case to []interface{} unless the slice is exactly that type. - rawV := reflect.ValueOf(raw) - switch rawV.Kind() { - case reflect.String: - // If raw and reified are equal, this is a string and should - // be rejected. - reified, reifiedOk := c.Get(k) - if reifiedOk && raw == reified && !c.IsComputed(k) { - return nil, []error{fmt.Errorf("%s: should be a map", k)} - } - // Otherwise it's likely raw is an interpolation. - return nil, nil - case reflect.Map: - case reflect.Slice: - default: - return nil, []error{fmt.Errorf("%s: should be a map", k)} - } - - // If it is not a slice, validate directly - if rawV.Kind() != reflect.Slice { - mapIface := rawV.Interface() - if _, errs := validateMapValues(k, mapIface.(map[string]interface{}), schema); len(errs) > 0 { - return nil, errs - } - if schema.ValidateFunc != nil { - return schema.ValidateFunc(mapIface, k) - } - return nil, nil - } - - // It is a slice, verify that all the elements are maps - raws := make([]interface{}, rawV.Len()) - for i, _ := range raws { - raws[i] = rawV.Index(i).Interface() - } - - for _, raw := range raws { - v := reflect.ValueOf(raw) - if v.Kind() != reflect.Map { - return nil, []error{fmt.Errorf( - "%s: should be a map", k)} - } - mapIface := v.Interface() - if _, errs := validateMapValues(k, mapIface.(map[string]interface{}), schema); len(errs) > 0 { - return nil, errs - } - } - - if schema.ValidateFunc != nil { - validatableMap := make(map[string]interface{}) - for _, raw := range raws { - for k, v := range raw.(map[string]interface{}) { - validatableMap[k] = v - } - } - - return schema.ValidateFunc(validatableMap, k) - } - - return nil, nil -} - -func validateMapValues(k string, m map[string]interface{}, schema *Schema) ([]string, []error) { - for key, raw := range m { - valueType, err := getValueType(k, schema) - if err != nil { - return nil, []error{err} - } - - switch valueType { - case TypeBool: - var n bool - if err := mapstructure.WeakDecode(raw, &n); err != nil { - return nil, []error{fmt.Errorf("%s (%s): %s", k, key, err)} - } - case TypeInt: - var n int - if err := mapstructure.WeakDecode(raw, &n); err != nil { - return nil, []error{fmt.Errorf("%s (%s): %s", k, key, err)} - } - case TypeFloat: - var n float64 - if err := mapstructure.WeakDecode(raw, &n); err != nil { - return nil, []error{fmt.Errorf("%s (%s): %s", k, key, err)} - } - case TypeString: - var n string - if err := mapstructure.WeakDecode(raw, &n); err != nil { - return nil, []error{fmt.Errorf("%s (%s): %s", k, key, err)} - } - default: - panic(fmt.Sprintf("Unknown validation type: %#v", schema.Type)) - } - } - return nil, nil -} - -func getValueType(k string, schema *Schema) (ValueType, error) { - if schema.Elem == nil { - return TypeString, nil - } - if vt, ok := schema.Elem.(ValueType); ok { - return vt, nil - } - - // If a Schema is provided to a Map, we use the Type of that schema - // as the type for each element in the Map. - if s, ok := schema.Elem.(*Schema); ok { - return s.Type, nil - } - - if _, ok := schema.Elem.(*Resource); ok { - // TODO: We don't actually support this (yet) - // but silently pass the validation, until we decide - // how to handle nested structures in maps - return TypeString, nil - } - return 0, fmt.Errorf("%s: unexpected map value type: %#v", k, schema.Elem) -} - -func (m schemaMap) validateObject( - k string, - schema map[string]*Schema, - c *terraform.ResourceConfig) ([]string, []error) { - raw, _ := c.Get(k) - - // schemaMap can't validate nil - if raw == nil { - return nil, nil - } - - if _, ok := raw.(map[string]interface{}); !ok && !c.IsComputed(k) { - return nil, []error{fmt.Errorf( - "%s: expected object, got %s", - k, reflect.ValueOf(raw).Kind())} - } - - var ws []string - var es []error - for subK, s := range schema { - key := subK - if k != "" { - key = fmt.Sprintf("%s.%s", k, subK) - } - - ws2, es2 := m.validate(key, s, c) - if len(ws2) > 0 { - ws = append(ws, ws2...) - } - if len(es2) > 0 { - es = append(es, es2...) - } - } - - // Detect any extra/unknown keys and report those as errors. - if m, ok := raw.(map[string]interface{}); ok { - for subk, _ := range m { - if _, ok := schema[subk]; !ok { - if subk == TimeoutsConfigKey { - continue - } - es = append(es, fmt.Errorf( - "%s: invalid or unknown key: %s", k, subk)) - } - } - } - - return ws, es -} - -func (m schemaMap) validatePrimitive( - k string, - raw interface{}, - schema *Schema, - c *terraform.ResourceConfig) ([]string, []error) { - - // a nil value shouldn't happen in the old protocol, and in the new - // protocol the types have already been validated. Either way, we can't - // reflect on nil, so don't panic. - if raw == nil { - return nil, nil - } - - // Catch if the user gave a complex type where a primitive was - // expected, so we can return a friendly error message that - // doesn't contain Go type system terminology. - switch reflect.ValueOf(raw).Type().Kind() { - case reflect.Slice: - return nil, []error{ - fmt.Errorf("%s must be a single value, not a list", k), - } - case reflect.Map: - return nil, []error{ - fmt.Errorf("%s must be a single value, not a map", k), - } - default: // ok - } - - if c.IsComputed(k) { - // If the key is being computed, then it is not an error as - // long as it's not a slice or map. - return nil, nil - } - - var decoded interface{} - switch schema.Type { - case TypeBool: - // Verify that we can parse this as the correct type - var n bool - if err := mapstructure.WeakDecode(raw, &n); err != nil { - return nil, []error{fmt.Errorf("%s: %s", k, err)} - } - decoded = n - case TypeInt: - switch { - case isProto5(): - // We need to verify the type precisely, because WeakDecode will - // decode a float as an integer. - - // the config shims only use int for integral number values - if v, ok := raw.(int); ok { - decoded = v - } else { - return nil, []error{fmt.Errorf("%s: must be a whole number, got %v", k, raw)} - } - default: - // Verify that we can parse this as an int - var n int - if err := mapstructure.WeakDecode(raw, &n); err != nil { - return nil, []error{fmt.Errorf("%s: %s", k, err)} - } - decoded = n - } - case TypeFloat: - // Verify that we can parse this as an int - var n float64 - if err := mapstructure.WeakDecode(raw, &n); err != nil { - return nil, []error{fmt.Errorf("%s: %s", k, err)} - } - decoded = n - case TypeString: - // Verify that we can parse this as a string - var n string - if err := mapstructure.WeakDecode(raw, &n); err != nil { - return nil, []error{fmt.Errorf("%s: %s", k, err)} - } - decoded = n - default: - panic(fmt.Sprintf("Unknown validation type: %#v", schema.Type)) - } - - if schema.ValidateFunc != nil { - return schema.ValidateFunc(decoded, k) - } - - return nil, nil -} - -func (m schemaMap) validateType( - k string, - raw interface{}, - schema *Schema, - c *terraform.ResourceConfig) ([]string, []error) { - var ws []string - var es []error - switch schema.Type { - case TypeSet, TypeList: - ws, es = m.validateList(k, raw, schema, c) - case TypeMap: - ws, es = m.validateMap(k, raw, schema, c) - default: - ws, es = m.validatePrimitive(k, raw, schema, c) - } - - if schema.Deprecated != "" { - ws = append(ws, fmt.Sprintf( - "%q: [DEPRECATED] %s", k, schema.Deprecated)) - } - - if schema.Removed != "" { - es = append(es, fmt.Errorf( - "%q: [REMOVED] %s", k, schema.Removed)) - } - - return ws, es -} - -// Zero returns the zero value for a type. -func (t ValueType) Zero() interface{} { - switch t { - case TypeInvalid: - return nil - case TypeBool: - return false - case TypeInt: - return 0 - case TypeFloat: - return 0.0 - case TypeString: - return "" - case TypeList: - return []interface{}{} - case TypeMap: - return map[string]interface{}{} - case TypeSet: - return new(Set) - case typeObject: - return map[string]interface{}{} - default: - panic(fmt.Sprintf("unknown type %s", t)) - } -} diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go deleted file mode 100644 index 4199f3ddf..000000000 --- a/helper/schema/schema_test.go +++ /dev/null @@ -1,5558 +0,0 @@ -package schema - -import ( - "bytes" - "errors" - "fmt" - "os" - "reflect" - "sort" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/helper/hashcode" - "github.com/hashicorp/terraform/terraform" -) - -func TestEnvDefaultFunc(t *testing.T) { - key := "TF_TEST_ENV_DEFAULT_FUNC" - defer os.Unsetenv(key) - - f := EnvDefaultFunc(key, "42") - if err := os.Setenv(key, "foo"); err != nil { - t.Fatalf("err: %s", err) - } - - actual, err := f() - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != "foo" { - t.Fatalf("bad: %#v", actual) - } - - if err := os.Unsetenv(key); err != nil { - t.Fatalf("err: %s", err) - } - - actual, err = f() - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != "42" { - t.Fatalf("bad: %#v", actual) - } -} - -func TestMultiEnvDefaultFunc(t *testing.T) { - keys := []string{ - "TF_TEST_MULTI_ENV_DEFAULT_FUNC1", - "TF_TEST_MULTI_ENV_DEFAULT_FUNC2", - } - defer func() { - for _, k := range keys { - os.Unsetenv(k) - } - }() - - // Test that the first key is returned first - f := MultiEnvDefaultFunc(keys, "42") - if err := os.Setenv(keys[0], "foo"); err != nil { - t.Fatalf("err: %s", err) - } - - actual, err := f() - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != "foo" { - t.Fatalf("bad: %#v", actual) - } - - if err := os.Unsetenv(keys[0]); err != nil { - t.Fatalf("err: %s", err) - } - - // Test that the second key is returned if the first one is empty - f = MultiEnvDefaultFunc(keys, "42") - if err := os.Setenv(keys[1], "foo"); err != nil { - t.Fatalf("err: %s", err) - } - - actual, err = f() - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != "foo" { - t.Fatalf("bad: %#v", actual) - } - - if err := os.Unsetenv(keys[1]); err != nil { - t.Fatalf("err: %s", err) - } - - // Test that the default value is returned when no keys are set - actual, err = f() - if err != nil { - t.Fatalf("err: %s", err) - } - if actual != "42" { - t.Fatalf("bad: %#v", actual) - } -} - -func TestValueType_Zero(t *testing.T) { - cases := []struct { - Type ValueType - Value interface{} - }{ - {TypeBool, false}, - {TypeInt, 0}, - {TypeFloat, 0.0}, - {TypeString, ""}, - {TypeList, []interface{}{}}, - {TypeMap, map[string]interface{}{}}, - {TypeSet, new(Set)}, - } - - for i, tc := range cases { - actual := tc.Type.Zero() - if !reflect.DeepEqual(actual, tc.Value) { - t.Fatalf("%d: %#v != %#v", i, actual, tc.Value) - } - } -} - -func TestSchemaMap_Diff(t *testing.T) { - cases := []struct { - Name string - Schema map[string]*Schema - State *terraform.InstanceState - Config map[string]interface{} - CustomizeDiff CustomizeDiffFunc - Diff *terraform.InstanceDiff - Err bool - }{ - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "foo", - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Computed, but set in config", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "bar", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - }, - }, - - Err: false, - }, - - { - Name: "Default", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Default: "foo", - }, - }, - - State: nil, - - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - - Err: false, - }, - - { - Name: "DefaultFunc, value", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - DefaultFunc: func() (interface{}, error) { - return "foo", nil - }, - }, - }, - - State: nil, - - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - - Err: false, - }, - - { - Name: "DefaultFunc, configuration set", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - DefaultFunc: func() (interface{}, error) { - return "foo", nil - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "bar", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - - Err: false, - }, - - { - Name: "String with StateFunc", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - StateFunc: func(a interface{}) string { - return a.(string) + "!" - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo!", - NewExtra: "foo", - }, - }, - }, - - Err: false, - }, - - { - Name: "StateFunc not called with nil value", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - StateFunc: func(a interface{}) string { - t.Fatalf("should not get here!") - return "" - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Variable computed", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": hcl2shim.UnknownVariableValue, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: hcl2shim.UnknownVariableValue, - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Int decode", - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeInt, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "port": 27, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "port": &terraform.ResourceAttrDiff{ - Old: "", - New: "27", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "bool decode", - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "port": false, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "port": &terraform.ResourceAttrDiff{ - Old: "", - New: "false", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Bool", - Schema: map[string]*Schema{ - "delete": &Schema{ - Type: TypeBool, - Optional: true, - Default: false, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "delete": "false", - }, - }, - - Config: nil, - - Diff: nil, - - Err: false, - }, - - { - Name: "List decode", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - }, - "ports.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "List decode with promotion", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - PromoteSingle: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": "5", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "ports.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "List decode with promotion with list", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - PromoteSingle: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{"5"}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "ports.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - }, - "ports.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, hcl2shim.UnknownVariableValue, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.0": "1", - "ports.1": "2", - "ports.2": "5", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: nil, - - Err: false, - }, - - { - Name: "", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "2", - "ports.0": "1", - "ports.1": "2", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "3", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - RequiresNew: true, - }, - "ports.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - RequiresNew: true, - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - RequiresNew: true, - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "List with computed set", - Schema: map[string]*Schema{ - "config": &Schema{ - Type: TypeList, - Optional: true, - ForceNew: true, - MinItems: 1, - Elem: &Resource{ - Schema: map[string]*Schema{ - "name": { - Type: TypeString, - Required: true, - }, - - "rules": { - Type: TypeSet, - Computed: true, - Elem: &Schema{Type: TypeString}, - Set: HashString, - }, - }, - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "config": []interface{}{ - map[string]interface{}{ - "name": "hello", - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - RequiresNew: true, - }, - - "config.0.name": &terraform.ResourceAttrDiff{ - Old: "", - New: "hello", - }, - - "config.0.rules.#": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{5, 2, 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Computed: true, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "0", - }, - }, - - Config: nil, - - Diff: nil, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{"2", "5", 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, hcl2shim.UnknownVariableValue, "5"}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "2", - "ports.1": "1", - "ports.2": "2", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{5, 2, 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "3", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "1", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "2", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "2", - "ports.1": "1", - "ports.2": "2", - }, - }, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "0", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "1", - New: "0", - NewRemoved: true, - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "2", - New: "0", - NewRemoved: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "bar", - "ports.#": "1", - "ports.80": "80", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - ps := m["ports"].([]interface{}) - result := 0 - for _, p := range ps { - result += p.(int) - } - return result - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ingress.#": "2", - "ingress.80.ports.#": "1", - "ingress.80.ports.0": "80", - "ingress.443.ports.#": "1", - "ingress.443.ports.0": "443", - }, - }, - - Config: map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "ports": []interface{}{443}, - }, - map[string]interface{}{ - "ports": []interface{}{80}, - }, - }, - }, - - Diff: nil, - - Err: false, - }, - - { - Name: "List of structure decode", - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "from": 8080, - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ingress.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "ingress.0.from": &terraform.ResourceAttrDiff{ - Old: "", - New: "8080", - }, - }, - }, - - Err: false, - }, - - { - Name: "ComputedWhen", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Computed: true, - ComputedWhen: []string{"port"}, - }, - - "port": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - "port": "80", - }, - }, - - Config: map[string]interface{}{ - "port": 80, - }, - - Diff: nil, - - Err: false, - }, - - { - Name: "", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Computed: true, - ComputedWhen: []string{"port"}, - }, - - "port": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "port": "80", - }, - }, - - Config: map[string]interface{}{ - "port": 80, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - /* TODO - { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Computed: true, - ComputedWhen: []string{"port"}, - }, - - "port": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "foo", - "port": "80", - }, - }, - - Config: map[string]interface{}{ - "port": 8080, - }, - - Diff: &terraform.ResourceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "foo", - NewComputed: true, - }, - "port": &terraform.ResourceAttrDiff{ - Old: "80", - New: "8080", - }, - }, - }, - - Err: false, - }, - */ - - { - Name: "Maps", - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeMap, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "config_vars": []interface{}{ - map[string]interface{}{ - "bar": "baz", - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.%": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - - "config_vars.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - - Err: false, - }, - - { - Name: "Maps", - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeMap, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.foo": "bar", - }, - }, - - Config: map[string]interface{}{ - "config_vars": []interface{}{ - map[string]interface{}{ - "bar": "baz", - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.foo": &terraform.ResourceAttrDiff{ - Old: "bar", - NewRemoved: true, - }, - "config_vars.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - - Err: false, - }, - - { - Name: "Maps", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "vars.foo": "bar", - }, - }, - - Config: map[string]interface{}{ - "vars": []interface{}{ - map[string]interface{}{ - "bar": "baz", - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "vars.foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "", - NewRemoved: true, - }, - "vars.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - - Err: false, - }, - - { - Name: "Maps", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "vars.foo": "bar", - }, - }, - - Config: nil, - - Diff: nil, - - Err: false, - }, - - { - Name: "Maps", - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeMap}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.#": "1", - "config_vars.0.foo": "bar", - }, - }, - - Config: map[string]interface{}{ - "config_vars": []interface{}{ - map[string]interface{}{ - "bar": "baz", - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.0.foo": &terraform.ResourceAttrDiff{ - Old: "bar", - NewRemoved: true, - }, - "config_vars.0.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - - Err: false, - }, - - { - Name: "Maps", - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeMap}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "config_vars.#": "1", - "config_vars.0.foo": "bar", - "config_vars.0.bar": "baz", - }, - }, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "0", - }, - "config_vars.0.%": &terraform.ResourceAttrDiff{ - Old: "2", - New: "0", - }, - "config_vars.0.foo": &terraform.ResourceAttrDiff{ - Old: "bar", - NewRemoved: true, - }, - "config_vars.0.bar": &terraform.ResourceAttrDiff{ - Old: "baz", - NewRemoved: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "ForceNews", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - ForceNew: true, - }, - - "address": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "bar", - "address": "foo", - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "foo", - RequiresNew: true, - }, - - "address": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - ForceNew: true, - }, - - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "availability_zone": "bar", - "ports.#": "1", - "ports.80": "80", - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "foo", - RequiresNew: true, - }, - - "ports.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "instances": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeString}, - Optional: true, - Computed: true, - Set: func(v interface{}) int { - return len(v.(string)) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "instances.#": "0", - }, - }, - - Config: map[string]interface{}{ - "instances": []interface{}{hcl2shim.UnknownVariableValue}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "instances.#": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "route": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "gateway": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "route": []interface{}{ - map[string]interface{}{ - "index": "1", - "gateway": hcl2shim.UnknownVariableValue, - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "route.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "route.~1.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "route.~1.gateway": &terraform.ResourceAttrDiff{ - Old: "", - New: hcl2shim.UnknownVariableValue, - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set", - Schema: map[string]*Schema{ - "route": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "gateway": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "route": []interface{}{ - map[string]interface{}{ - "index": "1", - "gateway": []interface{}{ - hcl2shim.UnknownVariableValue, - }, - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "route.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "route.~1.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "route.~1.gateway.#": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Computed maps", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - Computed: true, - }, - }, - - State: nil, - - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "vars.%": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Computed maps", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "vars.%": "0", - }, - }, - - Config: map[string]interface{}{ - "vars": map[string]interface{}{ - "bar": hcl2shim.UnknownVariableValue, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "vars.%": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: " - Empty", - Schema: map[string]*Schema{}, - - State: &terraform.InstanceState{}, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Float", - Schema: map[string]*Schema{ - "some_threshold": &Schema{ - Type: TypeFloat, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "some_threshold": "567.8", - }, - }, - - Config: map[string]interface{}{ - "some_threshold": 12.34, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "some_threshold": &terraform.ResourceAttrDiff{ - Old: "567.8", - New: "12.34", - }, - }, - }, - - Err: false, - }, - - { - Name: "https://github.com/hashicorp/terraform/issues/824", - Schema: map[string]*Schema{ - "block_device": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "device_name": &Schema{ - Type: TypeString, - Required: true, - }, - "delete_on_termination": &Schema{ - Type: TypeBool, - Optional: true, - Default: true, - }, - }, - }, - Set: func(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) - buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) - return hashcode.String(buf.String()) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "block_device.#": "2", - "block_device.616397234.delete_on_termination": "true", - "block_device.616397234.device_name": "/dev/sda1", - "block_device.2801811477.delete_on_termination": "true", - "block_device.2801811477.device_name": "/dev/sdx", - }, - }, - - Config: map[string]interface{}{ - "block_device": []interface{}{ - map[string]interface{}{ - "device_name": "/dev/sda1", - }, - map[string]interface{}{ - "device_name": "/dev/sdx", - }, - }, - }, - Diff: nil, - Err: false, - }, - - { - Name: "Zero value in state shouldn't result in diff", - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeBool, - Optional: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "port": "false", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Same as prev, but for sets", - Schema: map[string]*Schema{ - "route": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "gateway": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "route.#": "0", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "A set computed element shouldn't cause a diff", - Schema: map[string]*Schema{ - "active": &Schema{ - Type: TypeBool, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "active": "true", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "An empty set should show up in the diff", - Schema: map[string]*Schema{ - "instances": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeString}, - Optional: true, - ForceNew: true, - Set: func(v interface{}) int { - return len(v.(string)) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "instances.#": "1", - "instances.3": "foo", - }, - }, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "instances.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "0", - RequiresNew: true, - }, - "instances.3": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "", - NewRemoved: true, - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Map with empty value", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "vars": map[string]interface{}{ - "foo": "", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "vars.%": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "vars.foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "", - }, - }, - }, - - Err: false, - }, - - { - Name: "Unset bool, not in state", - Schema: map[string]*Schema{ - "force": &Schema{ - Type: TypeBool, - Optional: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Unset set, not in state", - Schema: map[string]*Schema{ - "metadata_keys": &Schema{ - Type: TypeSet, - Optional: true, - ForceNew: true, - Elem: &Schema{Type: TypeInt}, - Set: func(interface{}) int { return 0 }, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Unset list in state, should not show up computed", - Schema: map[string]*Schema{ - "metadata_keys": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - ForceNew: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "metadata_keys.#": "0", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Set element computed element", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, hcl2shim.UnknownVariableValue}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Computed map without config that's known to be empty does not generate diff", - Schema: map[string]*Schema{ - "tags": &Schema{ - Type: TypeMap, - Computed: true, - }, - }, - - Config: nil, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "tags.%": "0", - }, - }, - - Diff: nil, - - Err: false, - }, - - { - Name: "Set with hyphen keys", - Schema: map[string]*Schema{ - "route": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "gateway-name": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "route": []interface{}{ - map[string]interface{}{ - "index": "1", - "gateway-name": "hello", - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "route.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "route.1.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "route.1.gateway-name": &terraform.ResourceAttrDiff{ - Old: "", - New: "hello", - }, - }, - }, - - Err: false, - }, - - { - Name: ": StateFunc in nested set (#1759)", - Schema: map[string]*Schema{ - "service_account": &Schema{ - Type: TypeList, - Optional: true, - ForceNew: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "scopes": &Schema{ - Type: TypeSet, - Required: true, - ForceNew: true, - Elem: &Schema{ - Type: TypeString, - StateFunc: func(v interface{}) string { - return v.(string) + "!" - }, - }, - Set: func(v interface{}) int { - i, err := strconv.Atoi(v.(string)) - if err != nil { - t.Fatalf("err: %s", err) - } - return i - }, - }, - }, - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "service_account": []interface{}{ - map[string]interface{}{ - "scopes": []interface{}{"123"}, - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "service_account.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - RequiresNew: true, - }, - "service_account.0.scopes.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - RequiresNew: true, - }, - "service_account.0.scopes.123": &terraform.ResourceAttrDiff{ - Old: "", - New: "123!", - NewExtra: "123", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Removing set elements", - Schema: map[string]*Schema{ - "instances": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeString}, - Optional: true, - ForceNew: true, - Set: func(v interface{}) int { - return len(v.(string)) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "instances.#": "2", - "instances.3": "333", - "instances.2": "22", - }, - }, - - Config: map[string]interface{}{ - "instances": []interface{}{"333", "4444"}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "instances.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "2", - }, - "instances.2": &terraform.ResourceAttrDiff{ - Old: "22", - New: "", - NewRemoved: true, - RequiresNew: true, - }, - "instances.3": &terraform.ResourceAttrDiff{ - Old: "333", - New: "333", - }, - "instances.4": &terraform.ResourceAttrDiff{ - Old: "", - New: "4444", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Bools can be set with 0/1 in config, still get true/false", - Schema: map[string]*Schema{ - "one": &Schema{ - Type: TypeBool, - Optional: true, - }, - "two": &Schema{ - Type: TypeBool, - Optional: true, - }, - "three": &Schema{ - Type: TypeBool, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "one": "false", - "two": "true", - "three": "true", - }, - }, - - Config: map[string]interface{}{ - "one": "1", - "two": "0", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "one": &terraform.ResourceAttrDiff{ - Old: "false", - New: "true", - }, - "two": &terraform.ResourceAttrDiff{ - Old: "true", - New: "false", - }, - "three": &terraform.ResourceAttrDiff{ - Old: "true", - New: "false", - NewRemoved: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "tainted in state w/ no attr changes is still a replacement", - Schema: map[string]*Schema{}, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "id": "someid", - }, - Tainted: true, - }, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - DestroyTainted: true, - }, - - Err: false, - }, - - { - Name: "Set ForceNew only marks the changing element as ForceNew", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - ForceNew: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.1": "1", - "ports.2": "2", - "ports.4": "4", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{5, 2, 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "3", - New: "3", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "1", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "2", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - RequiresNew: true, - }, - "ports.4": &terraform.ResourceAttrDiff{ - Old: "4", - New: "0", - NewRemoved: true, - RequiresNew: true, - }, - }, - }, - }, - - { - Name: "removed optional items should trigger ForceNew", - Schema: map[string]*Schema{ - "description": &Schema{ - Type: TypeString, - ForceNew: true, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "description": "foo", - }, - }, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "description": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "", - RequiresNew: true, - NewRemoved: true, - }, - }, - }, - - Err: false, - }, - - // GH-7715 - { - Name: "computed value for boolean field", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeBool, - ForceNew: true, - Computed: true, - Optional: true, - }, - }, - - State: &terraform.InstanceState{}, - - Config: map[string]interface{}{ - "foo": hcl2shim.UnknownVariableValue, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "false", - NewComputed: true, - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set ForceNew marks count as ForceNew if computed", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - ForceNew: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.1": "1", - "ports.2": "2", - "ports.4": "4", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{hcl2shim.UnknownVariableValue, 2, 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "3", - New: "", - NewComputed: true, - RequiresNew: true, - }, - }, - }, - }, - - { - Name: "List with computed schema and ForceNew", - Schema: map[string]*Schema{ - "config": &Schema{ - Type: TypeList, - Optional: true, - ForceNew: true, - Elem: &Schema{ - Type: TypeString, - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "config.#": "2", - "config.0": "a", - "config.1": "b", - }, - }, - - Config: map[string]interface{}{ - "config": []interface{}{hcl2shim.UnknownVariableValue, hcl2shim.UnknownVariableValue}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "", - RequiresNew: true, - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "overridden diff with a CustomizeDiff function, ForceNew not in schema", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("availability_zone", "bar"); err != nil { - return err - } - if err := d.ForceNew("availability_zone"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - // NOTE: This case is technically impossible in the current - // implementation, because optional+computed values never show up in the - // diff. In the event behavior changes this test should ensure that the - // intended diff still shows up. - Name: "overridden removed attribute diff with a CustomizeDiff function, ForceNew not in schema", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("availability_zone", "bar"); err != nil { - return err - } - if err := d.ForceNew("availability_zone"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - - Name: "overridden diff with a CustomizeDiff function, ForceNew in schema", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("availability_zone", "bar"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "required field with computed diff added with CustomizeDiff function", - Schema: map[string]*Schema{ - "ami_id": &Schema{ - Type: TypeString, - Required: true, - }, - "instance_id": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ami_id": "foo", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("instance_id", "bar"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ami_id": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "instance_id": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - - Err: false, - }, - - { - Name: "Set ForceNew only marks the changing element as ForceNew - CustomizeDiffFunc edition", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "ports.#": "3", - "ports.1": "1", - "ports.2": "2", - "ports.4": "4", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{5, 2, 6}, - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("ports", []interface{}{5, 2, 1}); err != nil { - return err - } - if err := d.ForceNew("ports"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "3", - New: "3", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "1", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "2", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - RequiresNew: true, - }, - "ports.4": &terraform.ResourceAttrDiff{ - Old: "4", - New: "0", - NewRemoved: true, - RequiresNew: true, - }, - }, - }, - }, - - { - Name: "tainted resource does not run CustomizeDiffFunc", - Schema: map[string]*Schema{}, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "id": "someid", - }, - Tainted: true, - }, - - Config: map[string]interface{}{}, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - return errors.New("diff customization should not have run") - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - DestroyTainted: true, - }, - - Err: false, - }, - - { - Name: "NewComputed based on a conditional with CustomizeDiffFunc", - Schema: map[string]*Schema{ - "etag": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - "version_id": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "etag": "foo", - "version_id": "1", - }, - }, - - Config: map[string]interface{}{ - "etag": "bar", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if d.HasChange("etag") { - d.SetNewComputed("version_id") - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "etag": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - "version_id": &terraform.ResourceAttrDiff{ - Old: "1", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "NewComputed should always propagate with CustomizeDiff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "", - }, - ID: "pre-existing", - }, - - Config: map[string]interface{}{}, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - d.SetNewComputed("foo") - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "vetoing a diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "foo": "bar", - }, - }, - - Config: map[string]interface{}{ - "foo": "baz", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - return fmt.Errorf("diff vetoed") - }, - - Err: true, - }, - - // A lot of resources currently depended on using the empty string as a - // nil/unset value. - // FIXME: We want this to eventually produce a diff, since there - // technically is a new value in the config. - { - Name: "optional, computed, empty string", - Schema: map[string]*Schema{ - "attr": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "attr": "bar", - }, - }, - - Config: map[string]interface{}{ - "attr": "", - }, - }, - - { - Name: "optional, computed, empty string should not crash in CustomizeDiff", - Schema: map[string]*Schema{ - "unrelated_set": { - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeString}, - }, - "stream_enabled": { - Type: TypeBool, - Optional: true, - }, - "stream_view_type": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - Attributes: map[string]string{ - "unrelated_set.#": "0", - "stream_enabled": "true", - "stream_view_type": "KEYS_ONLY", - }, - }, - Config: map[string]interface{}{ - "stream_enabled": false, - "stream_view_type": "", - }, - CustomizeDiff: func(diff *ResourceDiff, v interface{}) error { - v, ok := diff.GetOk("unrelated_set") - if ok { - return fmt.Errorf("Didn't expect unrelated_set: %#v", v) - } - return nil - }, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "stream_enabled": { - Old: "true", - New: "false", - }, - }, - }, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - c := terraform.NewResourceConfigRaw(tc.Config) - - d, err := schemaMap(tc.Schema).Diff(tc.State, c, tc.CustomizeDiff, nil, true) - if err != nil != tc.Err { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(tc.Diff, d) { - t.Fatalf("expected:\n%#v\n\ngot:\n%#v", tc.Diff, d) - } - }) - } -} - -func TestSchemaMap_Input(t *testing.T) { - cases := map[string]struct { - Schema map[string]*Schema - Config map[string]interface{} - Input map[string]string - Result map[string]interface{} - Err bool - }{ - /* - * String decode - */ - - "no input on optional field with no config": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Input: map[string]string{}, - Result: map[string]interface{}{}, - Err: false, - }, - - "input ignored when config has a value": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "bar", - }, - - Input: map[string]string{ - "availability_zone": "foo", - }, - - Result: map[string]interface{}{}, - - Err: false, - }, - - "input ignored when schema has a default": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Default: "foo", - Optional: true, - }, - }, - - Input: map[string]string{ - "availability_zone": "bar", - }, - - Result: map[string]interface{}{}, - - Err: false, - }, - - "input ignored when default function returns a value": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - DefaultFunc: func() (interface{}, error) { - return "foo", nil - }, - Optional: true, - }, - }, - - Input: map[string]string{ - "availability_zone": "bar", - }, - - Result: map[string]interface{}{}, - - Err: false, - }, - - "input ignored when default function returns an empty string": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Default: "", - Optional: true, - }, - }, - - Input: map[string]string{ - "availability_zone": "bar", - }, - - Result: map[string]interface{}{}, - - Err: false, - }, - - "input used when default function returns nil": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - DefaultFunc: func() (interface{}, error) { - return nil, nil - }, - Required: true, - }, - }, - - Input: map[string]string{ - "availability_zone": "bar", - }, - - Result: map[string]interface{}{ - "availability_zone": "bar", - }, - - Err: false, - }, - - "input not used when optional default function returns nil": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - DefaultFunc: func() (interface{}, error) { - return nil, nil - }, - Optional: true, - }, - }, - - Input: map[string]string{}, - Result: map[string]interface{}{}, - Err: false, - }, - } - - for i, tc := range cases { - if tc.Config == nil { - tc.Config = make(map[string]interface{}) - } - - input := new(terraform.MockUIInput) - input.InputReturnMap = tc.Input - - rc := terraform.NewResourceConfigRaw(tc.Config) - rc.Config = make(map[string]interface{}) - - actual, err := schemaMap(tc.Schema).Input(input, rc) - if err != nil != tc.Err { - t.Fatalf("#%v err: %s", i, err) - } - - if !reflect.DeepEqual(tc.Result, actual.Config) { - t.Fatalf("#%v: bad:\n\ngot: %#v\nexpected: %#v", i, actual.Config, tc.Result) - } - } -} - -func TestSchemaMap_InputDefault(t *testing.T) { - emptyConfig := make(map[string]interface{}) - rc := terraform.NewResourceConfigRaw(emptyConfig) - rc.Config = make(map[string]interface{}) - - input := new(terraform.MockUIInput) - input.InputFn = func(opts *terraform.InputOpts) (string, error) { - t.Fatalf("InputFn should not be called on: %#v", opts) - return "", nil - } - - schema := map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Default: "foo", - Optional: true, - }, - } - actual, err := schemaMap(schema).Input(input, rc) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := map[string]interface{}{} - - if !reflect.DeepEqual(expected, actual.Config) { - t.Fatalf("got: %#v\nexpected: %#v", actual.Config, expected) - } -} - -func TestSchemaMap_InputDeprecated(t *testing.T) { - emptyConfig := make(map[string]interface{}) - rc := terraform.NewResourceConfigRaw(emptyConfig) - rc.Config = make(map[string]interface{}) - - input := new(terraform.MockUIInput) - input.InputFn = func(opts *terraform.InputOpts) (string, error) { - t.Fatalf("InputFn should not be called on: %#v", opts) - return "", nil - } - - schema := map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Deprecated: "long gone", - Optional: true, - }, - } - actual, err := schemaMap(schema).Input(input, rc) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := map[string]interface{}{} - - if !reflect.DeepEqual(expected, actual.Config) { - t.Fatalf("got: %#v\nexpected: %#v", actual.Config, expected) - } -} - -func TestSchemaMap_InternalValidate(t *testing.T) { - cases := map[string]struct { - In map[string]*Schema - Err bool - }{ - "nothing": { - nil, - false, - }, - - "Both optional and required": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - Required: true, - }, - }, - true, - }, - - "No optional and no required": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - }, - }, - true, - }, - - "Missing Type": { - map[string]*Schema{ - "foo": &Schema{ - Required: true, - }, - }, - true, - }, - - "Required but computed": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Required: true, - Computed: true, - }, - }, - true, - }, - - "Looks good": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Required: true, - }, - }, - false, - }, - - "Computed but has default": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - Computed: true, - Default: "foo", - }, - }, - true, - }, - - "Required but has default": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - Required: true, - Default: "foo", - }, - }, - true, - }, - - "List element not set": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - }, - }, - true, - }, - - "List default": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - Default: "foo", - }, - }, - true, - }, - - "List element computed": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Schema{ - Type: TypeInt, - Computed: true, - }, - }, - }, - true, - }, - - "List element with Set set": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - Set: func(interface{}) int { return 0 }, - Optional: true, - }, - }, - true, - }, - - "Set element with no Set set": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeInt}, - Optional: true, - }, - }, - false, - }, - - "Required but computedWhen": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Required: true, - ComputedWhen: []string{"foo"}, - }, - }, - true, - }, - - "Conflicting attributes cannot be required": { - map[string]*Schema{ - "a": &Schema{ - Type: TypeBool, - Required: true, - }, - "b": &Schema{ - Type: TypeBool, - Optional: true, - ConflictsWith: []string{"a"}, - }, - }, - true, - }, - - "Attribute with conflicts cannot be required": { - map[string]*Schema{ - "b": &Schema{ - Type: TypeBool, - Required: true, - ConflictsWith: []string{"a"}, - }, - }, - true, - }, - - "ConflictsWith cannot be used w/ ComputedWhen": { - map[string]*Schema{ - "a": &Schema{ - Type: TypeBool, - ComputedWhen: []string{"foor"}, - }, - "b": &Schema{ - Type: TypeBool, - Required: true, - ConflictsWith: []string{"a"}, - }, - }, - true, - }, - - "Sub-resource invalid": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "foo": new(Schema), - }, - }, - }, - }, - true, - }, - - "Sub-resource valid": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - }, - }, - }, - false, - }, - - "ValidateFunc on non-primitive": { - map[string]*Schema{ - "foo": &Schema{ - Type: TypeSet, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - return - }, - }, - }, - true, - }, - - "computed-only field with validateFunc": { - map[string]*Schema{ - "string": &Schema{ - Type: TypeString, - Computed: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - es = append(es, fmt.Errorf("this is not fine")) - return - }, - }, - }, - true, - }, - - "computed-only field with diffSuppressFunc": { - map[string]*Schema{ - "string": &Schema{ - Type: TypeString, - Computed: true, - DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { - // Always suppress any diff - return false - }, - }, - }, - true, - }, - - "invalid field name format #1": { - map[string]*Schema{ - "with space": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - true, - }, - - "invalid field name format #2": { - map[string]*Schema{ - "WithCapitals": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - true, - }, - - "invalid field name format of a Deprecated field": { - map[string]*Schema{ - "WithCapitals": &Schema{ - Type: TypeString, - Optional: true, - Deprecated: "Use with_underscores instead", - }, - }, - false, - }, - - "invalid field name format of a Removed field": { - map[string]*Schema{ - "WithCapitals": &Schema{ - Type: TypeString, - Optional: true, - Removed: "Use with_underscores instead", - }, - }, - false, - }, - - "ConfigModeBlock with Elem *Resource": { - map[string]*Schema{ - "block": &Schema{ - Type: TypeList, - ConfigMode: SchemaConfigModeBlock, - Optional: true, - Elem: &Resource{}, - }, - }, - false, - }, - - "ConfigModeBlock Computed with Elem *Resource": { - map[string]*Schema{ - "block": &Schema{ - Type: TypeList, - ConfigMode: SchemaConfigModeBlock, - Computed: true, - Elem: &Resource{}, - }, - }, - true, // ConfigMode of block cannot be used for computed schema - }, - - "ConfigModeBlock with Elem *Schema": { - map[string]*Schema{ - "block": &Schema{ - Type: TypeList, - ConfigMode: SchemaConfigModeBlock, - Optional: true, - Elem: &Schema{ - Type: TypeString, - }, - }, - }, - true, - }, - - "ConfigModeBlock with no Elem": { - map[string]*Schema{ - "block": &Schema{ - Type: TypeString, - ConfigMode: SchemaConfigModeBlock, - Optional: true, - }, - }, - true, - }, - - "ConfigModeBlock inside ConfigModeAttr": { - map[string]*Schema{ - "block": &Schema{ - Type: TypeList, - ConfigMode: SchemaConfigModeAttr, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "sub": &Schema{ - Type: TypeList, - ConfigMode: SchemaConfigModeBlock, - Elem: &Resource{}, - }, - }, - }, - }, - }, - true, // ConfigMode of block cannot be used in child of schema with ConfigMode of attribute - }, - - "ConfigModeAuto with *Resource inside ConfigModeAttr": { - map[string]*Schema{ - "block": &Schema{ - Type: TypeList, - ConfigMode: SchemaConfigModeAttr, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "sub": &Schema{ - Type: TypeList, - Elem: &Resource{}, - }, - }, - }, - }, - }, - true, // in *schema.Resource with ConfigMode of attribute, so must also have ConfigMode of attribute - }, - } - - for tn, tc := range cases { - t.Run(tn, func(t *testing.T) { - err := schemaMap(tc.In).InternalValidate(nil) - if err != nil != tc.Err { - if tc.Err { - t.Fatalf("%q: Expected error did not occur:\n\n%#v", tn, tc.In) - } - t.Fatalf("%q: Unexpected error occurred: %s\n\n%#v", tn, err, tc.In) - } - }) - } - -} - -func TestSchemaMap_DiffSuppress(t *testing.T) { - cases := map[string]struct { - Schema map[string]*Schema - State *terraform.InstanceState - Config map[string]interface{} - ExpectedDiff *terraform.InstanceDiff - Err bool - }{ - "#0 - Suppress otherwise valid diff by returning true": { - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { - // Always suppress any diff - return true - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - ExpectedDiff: nil, - - Err: false, - }, - - "#1 - Don't suppress diff by returning false": { - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { - // Always suppress any diff - return false - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - ExpectedDiff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "foo", - }, - }, - }, - - Err: false, - }, - - "Default with suppress makes no diff": { - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Default: "foo", - DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { - return true - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - ExpectedDiff: nil, - - Err: false, - }, - - "Default with false suppress makes diff": { - Schema: map[string]*Schema{ - "availability_zone": { - Type: TypeString, - Optional: true, - Default: "foo", - DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool { - return false - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - ExpectedDiff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": { - Old: "", - New: "foo", - }, - }, - }, - - Err: false, - }, - - "Complex structure with set of computed string should mark root set as computed": { - Schema: map[string]*Schema{ - "outer": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "outer_str": &Schema{ - Type: TypeString, - Optional: true, - }, - "inner": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "inner_str": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - }, - Set: func(v interface{}) int { - return 2 - }, - }, - }, - }, - Set: func(v interface{}) int { - return 1 - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "outer": []interface{}{ - map[string]interface{}{ - "outer_str": "foo", - "inner": []interface{}{ - map[string]interface{}{ - "inner_str": hcl2shim.UnknownVariableValue, - }, - }, - }, - }, - }, - - ExpectedDiff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "outer.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "outer.~1.outer_str": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "outer.~1.inner.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "outer.~1.inner.~2.inner_str": &terraform.ResourceAttrDiff{ - Old: "", - New: hcl2shim.UnknownVariableValue, - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - "Complex structure with complex list of computed string should mark root set as computed": { - Schema: map[string]*Schema{ - "outer": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "outer_str": &Schema{ - Type: TypeString, - Optional: true, - }, - "inner": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "inner_str": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - }, - }, - }, - }, - Set: func(v interface{}) int { - return 1 - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "outer": []interface{}{ - map[string]interface{}{ - "outer_str": "foo", - "inner": []interface{}{ - map[string]interface{}{ - "inner_str": hcl2shim.UnknownVariableValue, - }, - }, - }, - }, - }, - - ExpectedDiff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "outer.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "outer.~1.outer_str": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "outer.~1.inner.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "outer.~1.inner.0.inner_str": &terraform.ResourceAttrDiff{ - Old: "", - New: hcl2shim.UnknownVariableValue, - NewComputed: true, - }, - }, - }, - - Err: false, - }, - } - - for tn, tc := range cases { - t.Run(tn, func(t *testing.T) { - c := terraform.NewResourceConfigRaw(tc.Config) - - d, err := schemaMap(tc.Schema).Diff(tc.State, c, nil, nil, true) - if err != nil != tc.Err { - t.Fatalf("#%q err: %s", tn, err) - } - - if !reflect.DeepEqual(tc.ExpectedDiff, d) { - t.Fatalf("#%q:\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.ExpectedDiff, d) - } - }) - } -} - -func TestSchemaMap_Validate(t *testing.T) { - cases := map[string]struct { - Schema map[string]*Schema - Config map[string]interface{} - Err bool - Errors []error - Warnings []string - }{ - "Good": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - }, - - "Good, because the var is not set and that error will come elsewhere": { - Schema: map[string]*Schema{ - "size": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - - Config: map[string]interface{}{ - "size": hcl2shim.UnknownVariableValue, - }, - }, - - "Required field not set": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Required: true, - }, - }, - - Config: map[string]interface{}{}, - - Err: true, - }, - - "Invalid basic type": { - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - - Config: map[string]interface{}{ - "port": "I am invalid", - }, - - Err: true, - }, - - "Invalid complex type": { - Schema: map[string]*Schema{ - "user_data": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Config: map[string]interface{}{ - "user_data": []interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - }, - }, - - Err: true, - }, - - "Bad type": { - Schema: map[string]*Schema{ - "size": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - - Config: map[string]interface{}{ - "size": "nope", - }, - - Err: true, - }, - - "Required but has DefaultFunc": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Required: true, - DefaultFunc: func() (interface{}, error) { - return "foo", nil - }, - }, - }, - - Config: nil, - }, - - "Required but has DefaultFunc return nil": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Required: true, - DefaultFunc: func() (interface{}, error) { - return nil, nil - }, - }, - }, - - Config: nil, - - Err: true, - }, - - "List with promotion": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - PromoteSingle: true, - Optional: true, - }, - }, - - Config: map[string]interface{}{ - "ingress": "5", - }, - - Err: false, - }, - - "List with promotion set as list": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeInt}, - PromoteSingle: true, - Optional: true, - }, - }, - - Config: map[string]interface{}{ - "ingress": []interface{}{"5"}, - }, - - Err: false, - }, - - "Optional sub-resource": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - Config: map[string]interface{}{}, - - Err: false, - }, - - "Sub-resource is the wrong type": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - Config: map[string]interface{}{ - "ingress": []interface{}{"foo"}, - }, - - Err: true, - }, - - "Not a list nested block": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - Config: map[string]interface{}{ - "ingress": "foo", - }, - - Err: true, - Errors: []error{ - fmt.Errorf(`ingress: should be a list`), - }, - }, - - "Not a list primitive": { - Schema: map[string]*Schema{ - "strings": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Schema{ - Type: TypeString, - }, - }, - }, - - Config: map[string]interface{}{ - "strings": "foo", - }, - - Err: true, - Errors: []error{ - fmt.Errorf(`strings: should be a list`), - }, - }, - - "Unknown list": { - Schema: map[string]*Schema{ - "strings": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Schema{ - Type: TypeString, - }, - }, - }, - - Config: map[string]interface{}{ - "strings": hcl2shim.UnknownVariableValue, - }, - - Err: false, - }, - - "Unknown + Deprecation": { - Schema: map[string]*Schema{ - "old_news": &Schema{ - Type: TypeString, - Optional: true, - Deprecated: "please use 'new_news' instead", - }, - }, - - Config: map[string]interface{}{ - "old_news": hcl2shim.UnknownVariableValue, - }, - - Warnings: []string{ - "\"old_news\": [DEPRECATED] please use 'new_news' instead", - }, - }, - - "Required sub-resource field": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - Config: map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{}, - }, - }, - - Err: true, - }, - - "Good sub-resource": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - Config: map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "from": 80, - }, - }, - }, - - Err: false, - }, - - "Good sub-resource, computed value": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - }, - }, - }, - - Config: map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "from": hcl2shim.UnknownVariableValue, - }, - }, - }, - - Err: false, - }, - - "Invalid/unknown field": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - Config: map[string]interface{}{ - "foo": "bar", - }, - - Err: true, - }, - - "Invalid/unknown field with computed value": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - Config: map[string]interface{}{ - "foo": hcl2shim.UnknownVariableValue, - }, - - Err: true, - }, - - "Computed field set": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "bar", - }, - - Err: true, - }, - - "Not a set": { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - Config: map[string]interface{}{ - "ports": "foo", - }, - - Err: true, - }, - - "Maps": { - Schema: map[string]*Schema{ - "user_data": &Schema{ - Type: TypeMap, - Optional: true, - }, - }, - - Config: map[string]interface{}{ - "user_data": "foo", - }, - - Err: true, - }, - - "Good map: data surrounded by extra slice": { - Schema: map[string]*Schema{ - "user_data": &Schema{ - Type: TypeMap, - Optional: true, - }, - }, - - Config: map[string]interface{}{ - "user_data": []interface{}{ - map[string]interface{}{ - "foo": "bar", - }, - }, - }, - }, - - "Good map": { - Schema: map[string]*Schema{ - "user_data": &Schema{ - Type: TypeMap, - Optional: true, - }, - }, - - Config: map[string]interface{}{ - "user_data": map[string]interface{}{ - "foo": "bar", - }, - }, - }, - - "Map with type specified as value type": { - Schema: map[string]*Schema{ - "user_data": &Schema{ - Type: TypeMap, - Optional: true, - Elem: TypeBool, - }, - }, - - Config: map[string]interface{}{ - "user_data": map[string]interface{}{ - "foo": "not_a_bool", - }, - }, - - Err: true, - }, - - "Map with type specified as nested Schema": { - Schema: map[string]*Schema{ - "user_data": &Schema{ - Type: TypeMap, - Optional: true, - Elem: &Schema{Type: TypeBool}, - }, - }, - - Config: map[string]interface{}{ - "user_data": map[string]interface{}{ - "foo": "not_a_bool", - }, - }, - - Err: true, - }, - - "Bad map: just a slice": { - Schema: map[string]*Schema{ - "user_data": &Schema{ - Type: TypeMap, - Optional: true, - }, - }, - - Config: map[string]interface{}{ - "user_data": []interface{}{ - "foo", - }, - }, - - Err: true, - }, - - "Good set: config has slice with single interpolated value": { - Schema: map[string]*Schema{ - "security_groups": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - ForceNew: true, - Elem: &Schema{Type: TypeString}, - Set: func(v interface{}) int { - return len(v.(string)) - }, - }, - }, - - Config: map[string]interface{}{ - "security_groups": []interface{}{"${var.foo}"}, - }, - - Err: false, - }, - - "Bad set: config has single interpolated value": { - Schema: map[string]*Schema{ - "security_groups": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - ForceNew: true, - Elem: &Schema{Type: TypeString}, - }, - }, - - Config: map[string]interface{}{ - "security_groups": "${var.foo}", - }, - - Err: true, - }, - - "Bad, subresource should not allow unknown elements": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - Config: map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "port": 80, - "other": "yes", - }, - }, - }, - - Err: true, - }, - - "Bad, subresource should not allow invalid types": { - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - Config: map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "port": "bad", - }, - }, - }, - - Err: true, - }, - - "Bad, should not allow lists to be assigned to string attributes": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Required: true, - }, - }, - - Config: map[string]interface{}{ - "availability_zone": []interface{}{"foo", "bar", "baz"}, - }, - - Err: true, - }, - - "Bad, should not allow maps to be assigned to string attributes": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Required: true, - }, - }, - - Config: map[string]interface{}{ - "availability_zone": map[string]interface{}{"foo": "bar", "baz": "thing"}, - }, - - Err: true, - }, - - "Deprecated attribute usage generates warning, but not error": { - Schema: map[string]*Schema{ - "old_news": &Schema{ - Type: TypeString, - Optional: true, - Deprecated: "please use 'new_news' instead", - }, - }, - - Config: map[string]interface{}{ - "old_news": "extra extra!", - }, - - Err: false, - - Warnings: []string{ - "\"old_news\": [DEPRECATED] please use 'new_news' instead", - }, - }, - - "Deprecated generates no warnings if attr not used": { - Schema: map[string]*Schema{ - "old_news": &Schema{ - Type: TypeString, - Optional: true, - Deprecated: "please use 'new_news' instead", - }, - }, - - Err: false, - - Warnings: nil, - }, - - "Removed attribute usage generates error": { - Schema: map[string]*Schema{ - "long_gone": &Schema{ - Type: TypeString, - Optional: true, - Removed: "no longer supported by Cloud API", - }, - }, - - Config: map[string]interface{}{ - "long_gone": "still here!", - }, - - Err: true, - Errors: []error{ - fmt.Errorf("\"long_gone\": [REMOVED] no longer supported by Cloud API"), - }, - }, - - "Removed generates no errors if attr not used": { - Schema: map[string]*Schema{ - "long_gone": &Schema{ - Type: TypeString, - Optional: true, - Removed: "no longer supported by Cloud API", - }, - }, - - Err: false, - }, - - "Conflicting attributes generate error": { - Schema: map[string]*Schema{ - "b": &Schema{ - Type: TypeString, - Optional: true, - }, - "a": &Schema{ - Type: TypeString, - Optional: true, - ConflictsWith: []string{"b"}, - }, - }, - - Config: map[string]interface{}{ - "b": "b-val", - "a": "a-val", - }, - - Err: true, - Errors: []error{ - fmt.Errorf("\"a\": conflicts with b"), - }, - }, - - "Conflicting attributes okay when unknown 1": { - Schema: map[string]*Schema{ - "b": &Schema{ - Type: TypeString, - Optional: true, - }, - "a": &Schema{ - Type: TypeString, - Optional: true, - ConflictsWith: []string{"b"}, - }, - }, - - Config: map[string]interface{}{ - "b": "b-val", - "a": hcl2shim.UnknownVariableValue, - }, - - Err: false, - }, - - "Conflicting attributes okay when unknown 2": { - Schema: map[string]*Schema{ - "b": &Schema{ - Type: TypeString, - Optional: true, - }, - "a": &Schema{ - Type: TypeString, - Optional: true, - ConflictsWith: []string{"b"}, - }, - }, - - Config: map[string]interface{}{ - "b": hcl2shim.UnknownVariableValue, - "a": "a-val", - }, - - Err: false, - }, - - "Conflicting attributes generate error even if one is unknown": { - Schema: map[string]*Schema{ - "b": &Schema{ - Type: TypeString, - Optional: true, - ConflictsWith: []string{"a", "c"}, - }, - "a": &Schema{ - Type: TypeString, - Optional: true, - ConflictsWith: []string{"b", "c"}, - }, - "c": &Schema{ - Type: TypeString, - Optional: true, - ConflictsWith: []string{"b", "a"}, - }, - }, - - Config: map[string]interface{}{ - "b": hcl2shim.UnknownVariableValue, - "a": "a-val", - "c": "c-val", - }, - - Err: true, - Errors: []error{ - fmt.Errorf("\"a\": conflicts with c"), - fmt.Errorf("\"c\": conflicts with a"), - }, - }, - - "Required attribute & undefined conflicting optional are good": { - Schema: map[string]*Schema{ - "required_att": &Schema{ - Type: TypeString, - Required: true, - }, - "optional_att": &Schema{ - Type: TypeString, - Optional: true, - ConflictsWith: []string{"required_att"}, - }, - }, - - Config: map[string]interface{}{ - "required_att": "required-val", - }, - - Err: false, - }, - - "Required conflicting attribute & defined optional generate error": { - Schema: map[string]*Schema{ - "required_att": &Schema{ - Type: TypeString, - Required: true, - }, - "optional_att": &Schema{ - Type: TypeString, - Optional: true, - ConflictsWith: []string{"required_att"}, - }, - }, - - Config: map[string]interface{}{ - "required_att": "required-val", - "optional_att": "optional-val", - }, - - Err: true, - Errors: []error{ - fmt.Errorf(`"optional_att": conflicts with required_att`), - }, - }, - - "Computed + Optional fields conflicting with each other": { - Schema: map[string]*Schema{ - "foo_att": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"bar_att"}, - }, - "bar_att": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"foo_att"}, - }, - }, - - Config: map[string]interface{}{ - "foo_att": "foo-val", - "bar_att": "bar-val", - }, - - Err: true, - Errors: []error{ - fmt.Errorf(`"foo_att": conflicts with bar_att`), - fmt.Errorf(`"bar_att": conflicts with foo_att`), - }, - }, - - "Computed + Optional fields NOT conflicting with each other": { - Schema: map[string]*Schema{ - "foo_att": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"bar_att"}, - }, - "bar_att": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"foo_att"}, - }, - }, - - Config: map[string]interface{}{ - "foo_att": "foo-val", - }, - - Err: false, - }, - - "Computed + Optional fields that conflict with none set": { - Schema: map[string]*Schema{ - "foo_att": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"bar_att"}, - }, - "bar_att": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"foo_att"}, - }, - }, - - Config: map[string]interface{}{}, - - Err: false, - }, - - "Good with ValidateFunc": { - Schema: map[string]*Schema{ - "validate_me": &Schema{ - Type: TypeString, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - return - }, - }, - }, - Config: map[string]interface{}{ - "validate_me": "valid", - }, - Err: false, - }, - - "Bad with ValidateFunc": { - Schema: map[string]*Schema{ - "validate_me": &Schema{ - Type: TypeString, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - es = append(es, fmt.Errorf("something is not right here")) - return - }, - }, - }, - Config: map[string]interface{}{ - "validate_me": "invalid", - }, - Err: true, - Errors: []error{ - fmt.Errorf(`something is not right here`), - }, - }, - - "ValidateFunc not called when type does not match": { - Schema: map[string]*Schema{ - "number": &Schema{ - Type: TypeInt, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - t.Fatalf("Should not have gotten validate call") - return - }, - }, - }, - Config: map[string]interface{}{ - "number": "NaN", - }, - Err: true, - }, - - "ValidateFunc gets decoded type": { - Schema: map[string]*Schema{ - "maybe": &Schema{ - Type: TypeBool, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - if _, ok := v.(bool); !ok { - t.Fatalf("Expected bool, got: %#v", v) - } - return - }, - }, - }, - Config: map[string]interface{}{ - "maybe": "true", - }, - }, - - "ValidateFunc is not called with a computed value": { - Schema: map[string]*Schema{ - "validate_me": &Schema{ - Type: TypeString, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - es = append(es, fmt.Errorf("something is not right here")) - return - }, - }, - }, - Config: map[string]interface{}{ - "validate_me": hcl2shim.UnknownVariableValue, - }, - - Err: false, - }, - - "special timeouts field": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - Config: map[string]interface{}{ - TimeoutsConfigKey: "bar", - }, - - Err: false, - }, - - "invalid bool field": { - Schema: map[string]*Schema{ - "bool_field": { - Type: TypeBool, - Optional: true, - }, - }, - Config: map[string]interface{}{ - "bool_field": "abcdef", - }, - Err: true, - }, - "invalid integer field": { - Schema: map[string]*Schema{ - "integer_field": { - Type: TypeInt, - Optional: true, - }, - }, - Config: map[string]interface{}{ - "integer_field": "abcdef", - }, - Err: true, - }, - "invalid float field": { - Schema: map[string]*Schema{ - "float_field": { - Type: TypeFloat, - Optional: true, - }, - }, - Config: map[string]interface{}{ - "float_field": "abcdef", - }, - Err: true, - }, - - // Invalid map values - "invalid bool map value": { - Schema: map[string]*Schema{ - "boolMap": &Schema{ - Type: TypeMap, - Elem: TypeBool, - Optional: true, - }, - }, - Config: map[string]interface{}{ - "boolMap": map[string]interface{}{ - "boolField": "notbool", - }, - }, - Err: true, - }, - "invalid int map value": { - Schema: map[string]*Schema{ - "intMap": &Schema{ - Type: TypeMap, - Elem: TypeInt, - Optional: true, - }, - }, - Config: map[string]interface{}{ - "intMap": map[string]interface{}{ - "intField": "notInt", - }, - }, - Err: true, - }, - "invalid float map value": { - Schema: map[string]*Schema{ - "floatMap": &Schema{ - Type: TypeMap, - Elem: TypeFloat, - Optional: true, - }, - }, - Config: map[string]interface{}{ - "floatMap": map[string]interface{}{ - "floatField": "notFloat", - }, - }, - Err: true, - }, - - "map with positive validate function": { - Schema: map[string]*Schema{ - "floatInt": &Schema{ - Type: TypeMap, - Elem: TypeInt, - Optional: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - return - }, - }, - }, - Config: map[string]interface{}{ - "floatInt": map[string]interface{}{ - "rightAnswer": "42", - "tooMuch": "43", - }, - }, - Err: false, - }, - "map with negative validate function": { - Schema: map[string]*Schema{ - "floatInt": &Schema{ - Type: TypeMap, - Elem: TypeInt, - Optional: true, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - es = append(es, fmt.Errorf("this is not fine")) - return - }, - }, - }, - Config: map[string]interface{}{ - "floatInt": map[string]interface{}{ - "rightAnswer": "42", - "tooMuch": "43", - }, - }, - Err: true, - }, - - // The Validation function should not see interpolation strings from - // non-computed values. - "set with partially computed list and map": { - Schema: map[string]*Schema{ - "outer": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "list": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Schema{ - Type: TypeString, - ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { - if strings.HasPrefix(v.(string), "${") { - es = append(es, fmt.Errorf("should not have interpolations")) - } - return - }, - }, - }, - }, - }, - }, - }, - Config: map[string]interface{}{ - "outer": []interface{}{ - map[string]interface{}{ - "list": []interface{}{"A", hcl2shim.UnknownVariableValue, "c"}, - }, - }, - }, - Err: false, - }, - "unexpected nils values": { - Schema: map[string]*Schema{ - "strings": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Schema{ - Type: TypeString, - }, - }, - "block": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "int": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - Config: map[string]interface{}{ - "strings": []interface{}{"1", nil}, - "block": []interface{}{map[string]interface{}{ - "int": nil, - }, - nil, - }, - }, - Err: true, - }, - } - - for tn, tc := range cases { - t.Run(tn, func(t *testing.T) { - c := terraform.NewResourceConfigRaw(tc.Config) - - ws, es := schemaMap(tc.Schema).Validate(c) - if len(es) > 0 != tc.Err { - if len(es) == 0 { - t.Errorf("%q: no errors", tn) - } - - for _, e := range es { - t.Errorf("%q: err: %s", tn, e) - } - - t.FailNow() - } - - if !reflect.DeepEqual(ws, tc.Warnings) { - t.Fatalf("%q: warnings:\n\nexpected: %#v\ngot:%#v", tn, tc.Warnings, ws) - } - - if tc.Errors != nil { - sort.Sort(errorSort(es)) - sort.Sort(errorSort(tc.Errors)) - - if !reflect.DeepEqual(es, tc.Errors) { - t.Fatalf("%q: errors:\n\nexpected: %q\ngot: %q", tn, tc.Errors, es) - } - } - }) - - } -} - -func TestSchemaSet_ValidateMaxItems(t *testing.T) { - cases := map[string]struct { - Schema map[string]*Schema - State *terraform.InstanceState - Config map[string]interface{} - ConfigVariables map[string]string - Diff *terraform.InstanceDiff - Err bool - Errors []error - }{ - "#0": { - Schema: map[string]*Schema{ - "aliases": &Schema{ - Type: TypeSet, - Optional: true, - MaxItems: 1, - Elem: &Schema{Type: TypeString}, - }, - }, - State: nil, - Config: map[string]interface{}{ - "aliases": []interface{}{"foo", "bar"}, - }, - Diff: nil, - Err: true, - Errors: []error{ - fmt.Errorf("aliases: attribute supports 1 item maximum, config has 2 declared"), - }, - }, - "#1": { - Schema: map[string]*Schema{ - "aliases": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeString}, - }, - }, - State: nil, - Config: map[string]interface{}{ - "aliases": []interface{}{"foo", "bar"}, - }, - Diff: nil, - Err: false, - Errors: nil, - }, - "#2": { - Schema: map[string]*Schema{ - "aliases": &Schema{ - Type: TypeSet, - Optional: true, - MaxItems: 1, - Elem: &Schema{Type: TypeString}, - }, - }, - State: nil, - Config: map[string]interface{}{ - "aliases": []interface{}{"foo"}, - }, - Diff: nil, - Err: false, - Errors: nil, - }, - } - - for tn, tc := range cases { - c := terraform.NewResourceConfigRaw(tc.Config) - _, es := schemaMap(tc.Schema).Validate(c) - - if len(es) > 0 != tc.Err { - if len(es) == 0 { - t.Errorf("%q: no errors", tn) - } - - for _, e := range es { - t.Errorf("%q: err: %s", tn, e) - } - - t.FailNow() - } - - if tc.Errors != nil { - if !reflect.DeepEqual(es, tc.Errors) { - t.Fatalf("%q: expected: %q\ngot: %q", tn, tc.Errors, es) - } - } - } -} - -func TestSchemaSet_ValidateMinItems(t *testing.T) { - cases := map[string]struct { - Schema map[string]*Schema - State *terraform.InstanceState - Config map[string]interface{} - ConfigVariables map[string]string - Diff *terraform.InstanceDiff - Err bool - Errors []error - }{ - "#0": { - Schema: map[string]*Schema{ - "aliases": &Schema{ - Type: TypeSet, - Optional: true, - MinItems: 2, - Elem: &Schema{Type: TypeString}, - }, - }, - State: nil, - Config: map[string]interface{}{ - "aliases": []interface{}{"foo", "bar"}, - }, - Diff: nil, - Err: false, - Errors: nil, - }, - "#1": { - Schema: map[string]*Schema{ - "aliases": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeString}, - }, - }, - State: nil, - Config: map[string]interface{}{ - "aliases": []interface{}{"foo", "bar"}, - }, - Diff: nil, - Err: false, - Errors: nil, - }, - "#2": { - Schema: map[string]*Schema{ - "aliases": &Schema{ - Type: TypeSet, - Optional: true, - MinItems: 2, - Elem: &Schema{Type: TypeString}, - }, - }, - State: nil, - Config: map[string]interface{}{ - "aliases": []interface{}{"foo"}, - }, - Diff: nil, - Err: true, - Errors: []error{ - fmt.Errorf("aliases: attribute supports 2 item as a minimum, config has 1 declared"), - }, - }, - } - - for tn, tc := range cases { - c := terraform.NewResourceConfigRaw(tc.Config) - _, es := schemaMap(tc.Schema).Validate(c) - - if len(es) > 0 != tc.Err { - if len(es) == 0 { - t.Errorf("%q: no errors", tn) - } - - for _, e := range es { - t.Errorf("%q: err: %s", tn, e) - } - - t.FailNow() - } - - if tc.Errors != nil { - if !reflect.DeepEqual(es, tc.Errors) { - t.Fatalf("%q: expected: %q\ngot: %q", tn, tc.Errors, es) - } - } - } -} - -// errorSort implements sort.Interface to sort errors by their error message -type errorSort []error - -func (e errorSort) Len() int { return len(e) } -func (e errorSort) Swap(i, j int) { e[i], e[j] = e[j], e[i] } -func (e errorSort) Less(i, j int) bool { - return e[i].Error() < e[j].Error() -} - -func TestSchemaMapDeepCopy(t *testing.T) { - schema := map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - }, - } - source := schemaMap(schema) - dest := source.DeepCopy() - dest["foo"].ForceNew = true - if reflect.DeepEqual(source, dest) { - t.Fatalf("source and dest should not match") - } -} diff --git a/helper/schema/serialize.go b/helper/schema/serialize.go deleted file mode 100644 index fe6d7504c..000000000 --- a/helper/schema/serialize.go +++ /dev/null @@ -1,125 +0,0 @@ -package schema - -import ( - "bytes" - "fmt" - "sort" - "strconv" -) - -func SerializeValueForHash(buf *bytes.Buffer, val interface{}, schema *Schema) { - if val == nil { - buf.WriteRune(';') - return - } - - switch schema.Type { - case TypeBool: - if val.(bool) { - buf.WriteRune('1') - } else { - buf.WriteRune('0') - } - case TypeInt: - buf.WriteString(strconv.Itoa(val.(int))) - case TypeFloat: - buf.WriteString(strconv.FormatFloat(val.(float64), 'g', -1, 64)) - case TypeString: - buf.WriteString(val.(string)) - case TypeList: - buf.WriteRune('(') - l := val.([]interface{}) - for _, innerVal := range l { - serializeCollectionMemberForHash(buf, innerVal, schema.Elem) - } - buf.WriteRune(')') - case TypeMap: - - m := val.(map[string]interface{}) - var keys []string - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - buf.WriteRune('[') - for _, k := range keys { - innerVal := m[k] - if innerVal == nil { - continue - } - buf.WriteString(k) - buf.WriteRune(':') - - switch innerVal := innerVal.(type) { - case int: - buf.WriteString(strconv.Itoa(innerVal)) - case float64: - buf.WriteString(strconv.FormatFloat(innerVal, 'g', -1, 64)) - case string: - buf.WriteString(innerVal) - default: - panic(fmt.Sprintf("unknown value type in TypeMap %T", innerVal)) - } - - buf.WriteRune(';') - } - buf.WriteRune(']') - case TypeSet: - buf.WriteRune('{') - s := val.(*Set) - for _, innerVal := range s.List() { - serializeCollectionMemberForHash(buf, innerVal, schema.Elem) - } - buf.WriteRune('}') - default: - panic("unknown schema type to serialize") - } - buf.WriteRune(';') -} - -// SerializeValueForHash appends a serialization of the given resource config -// to the given buffer, guaranteeing deterministic results given the same value -// and schema. -// -// Its primary purpose is as input into a hashing function in order -// to hash complex substructures when used in sets, and so the serialization -// is not reversible. -func SerializeResourceForHash(buf *bytes.Buffer, val interface{}, resource *Resource) { - if val == nil { - return - } - sm := resource.Schema - m := val.(map[string]interface{}) - var keys []string - for k := range sm { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - innerSchema := sm[k] - // Skip attributes that are not user-provided. Computed attributes - // do not contribute to the hash since their ultimate value cannot - // be known at plan/diff time. - if !(innerSchema.Required || innerSchema.Optional) { - continue - } - - buf.WriteString(k) - buf.WriteRune(':') - innerVal := m[k] - SerializeValueForHash(buf, innerVal, innerSchema) - } -} - -func serializeCollectionMemberForHash(buf *bytes.Buffer, val interface{}, elem interface{}) { - switch tElem := elem.(type) { - case *Schema: - SerializeValueForHash(buf, val, tElem) - case *Resource: - buf.WriteRune('<') - SerializeResourceForHash(buf, val, tElem) - buf.WriteString(">;") - default: - panic(fmt.Sprintf("invalid element type: %T", tElem)) - } -} diff --git a/helper/schema/serialize_test.go b/helper/schema/serialize_test.go deleted file mode 100644 index 55afb1528..000000000 --- a/helper/schema/serialize_test.go +++ /dev/null @@ -1,238 +0,0 @@ -package schema - -import ( - "bytes" - "testing" -) - -func TestSerializeForHash(t *testing.T) { - type testCase struct { - Schema interface{} - Value interface{} - Expected string - } - - tests := []testCase{ - testCase{ - Schema: &Schema{ - Type: TypeInt, - }, - Value: 0, - Expected: "0;", - }, - - testCase{ - Schema: &Schema{ - Type: TypeInt, - }, - Value: 200, - Expected: "200;", - }, - - testCase{ - Schema: &Schema{ - Type: TypeBool, - }, - Value: true, - Expected: "1;", - }, - - testCase{ - Schema: &Schema{ - Type: TypeBool, - }, - Value: false, - Expected: "0;", - }, - - testCase{ - Schema: &Schema{ - Type: TypeFloat, - }, - Value: 1.0, - Expected: "1;", - }, - - testCase{ - Schema: &Schema{ - Type: TypeFloat, - }, - Value: 1.54, - Expected: "1.54;", - }, - - testCase{ - Schema: &Schema{ - Type: TypeFloat, - }, - Value: 0.1, - Expected: "0.1;", - }, - - testCase{ - Schema: &Schema{ - Type: TypeString, - }, - Value: "hello", - Expected: "hello;", - }, - - testCase{ - Schema: &Schema{ - Type: TypeString, - }, - Value: "1", - Expected: "1;", - }, - - testCase{ - Schema: &Schema{ - Type: TypeList, - Elem: &Schema{ - Type: TypeString, - }, - }, - Value: []interface{}{}, - Expected: "();", - }, - - testCase{ - Schema: &Schema{ - Type: TypeList, - Elem: &Schema{ - Type: TypeString, - }, - }, - Value: []interface{}{"hello", "world"}, - Expected: "(hello;world;);", - }, - - testCase{ - Schema: &Schema{ - Type: TypeList, - Elem: &Resource{ - Schema: map[string]*Schema{ - "fo": &Schema{ - Type: TypeString, - Required: true, - }, - "fum": &Schema{ - Type: TypeString, - Required: true, - }, - }, - }, - }, - Value: []interface{}{ - map[string]interface{}{ - "fo": "bar", - }, - map[string]interface{}{ - "fo": "baz", - "fum": "boz", - }, - }, - Expected: "(;;);", - }, - - testCase{ - Schema: &Schema{ - Type: TypeSet, - Elem: &Schema{ - Type: TypeString, - }, - }, - Value: NewSet(func(i interface{}) int { return len(i.(string)) }, []interface{}{ - "hello", - "woo", - }), - Expected: "{woo;hello;};", - }, - - testCase{ - Schema: &Schema{ - Type: TypeMap, - Elem: &Schema{ - Type: TypeString, - }, - }, - Value: map[string]interface{}{ - "foo": "bar", - "baz": "foo", - }, - Expected: "[baz:foo;foo:bar;];", - }, - - testCase{ - Schema: &Resource{ - Schema: map[string]*Schema{ - "name": &Schema{ - Type: TypeString, - Required: true, - }, - "size": &Schema{ - Type: TypeInt, - Optional: true, - }, - "green": &Schema{ - Type: TypeBool, - Optional: true, - Computed: true, - }, - "upside_down": &Schema{ - Type: TypeBool, - Computed: true, - }, - }, - }, - Value: map[string]interface{}{ - "name": "my-fun-database", - "size": 12, - "green": true, - }, - Expected: "green:1;name:my-fun-database;size:12;", - }, - - // test TypeMap nested in Schema: GH-7091 - testCase{ - Schema: &Resource{ - Schema: map[string]*Schema{ - "outer": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{ - Type: TypeMap, - Optional: true, - }, - }, - }, - }, - Value: map[string]interface{}{ - "outer": NewSet(func(i interface{}) int { return 42 }, []interface{}{ - map[string]interface{}{ - "foo": "bar", - "baz": "foo", - }, - }), - }, - Expected: "outer:{[baz:foo;foo:bar;];};", - }, - } - - for _, test := range tests { - var gotBuf bytes.Buffer - schema := test.Schema - - switch s := schema.(type) { - case *Schema: - SerializeValueForHash(&gotBuf, test.Value, s) - case *Resource: - SerializeResourceForHash(&gotBuf, test.Value, s) - } - - got := gotBuf.String() - if got != test.Expected { - t.Errorf("hash(%#v) got %#v, but want %#v", test.Value, got, test.Expected) - } - } -} diff --git a/helper/schema/set.go b/helper/schema/set.go deleted file mode 100644 index 8ee89e475..000000000 --- a/helper/schema/set.go +++ /dev/null @@ -1,250 +0,0 @@ -package schema - -import ( - "bytes" - "fmt" - "reflect" - "sort" - "strconv" - "sync" - - "github.com/hashicorp/terraform/helper/hashcode" -) - -// HashString hashes strings. If you want a Set of strings, this is the -// SchemaSetFunc you want. -func HashString(v interface{}) int { - return hashcode.String(v.(string)) -} - -// HashInt hashes integers. If you want a Set of integers, this is the -// SchemaSetFunc you want. -func HashInt(v interface{}) int { - return hashcode.String(strconv.Itoa(v.(int))) -} - -// HashResource hashes complex structures that are described using -// a *Resource. This is the default set implementation used when a set's -// element type is a full resource. -func HashResource(resource *Resource) SchemaSetFunc { - return func(v interface{}) int { - var buf bytes.Buffer - SerializeResourceForHash(&buf, v, resource) - return hashcode.String(buf.String()) - } -} - -// HashSchema hashes values that are described using a *Schema. This is the -// default set implementation used when a set's element type is a single -// schema. -func HashSchema(schema *Schema) SchemaSetFunc { - return func(v interface{}) int { - var buf bytes.Buffer - SerializeValueForHash(&buf, v, schema) - return hashcode.String(buf.String()) - } -} - -// Set is a set data structure that is returned for elements of type -// TypeSet. -type Set struct { - F SchemaSetFunc - - m map[string]interface{} - once sync.Once -} - -// NewSet is a convenience method for creating a new set with the given -// items. -func NewSet(f SchemaSetFunc, items []interface{}) *Set { - s := &Set{F: f} - for _, i := range items { - s.Add(i) - } - - return s -} - -// CopySet returns a copy of another set. -func CopySet(otherSet *Set) *Set { - return NewSet(otherSet.F, otherSet.List()) -} - -// Add adds an item to the set if it isn't already in the set. -func (s *Set) Add(item interface{}) { - s.add(item, false) -} - -// Remove removes an item if it's already in the set. Idempotent. -func (s *Set) Remove(item interface{}) { - s.remove(item) -} - -// Contains checks if the set has the given item. -func (s *Set) Contains(item interface{}) bool { - _, ok := s.m[s.hash(item)] - return ok -} - -// Len returns the amount of items in the set. -func (s *Set) Len() int { - return len(s.m) -} - -// List returns the elements of this set in slice format. -// -// The order of the returned elements is deterministic. Given the same -// set, the order of this will always be the same. -func (s *Set) List() []interface{} { - result := make([]interface{}, len(s.m)) - for i, k := range s.listCode() { - result[i] = s.m[k] - } - - return result -} - -// Difference performs a set difference of the two sets, returning -// a new third set that has only the elements unique to this set. -func (s *Set) Difference(other *Set) *Set { - result := &Set{F: s.F} - result.once.Do(result.init) - - for k, v := range s.m { - if _, ok := other.m[k]; !ok { - result.m[k] = v - } - } - - return result -} - -// Intersection performs the set intersection of the two sets -// and returns a new third set. -func (s *Set) Intersection(other *Set) *Set { - result := &Set{F: s.F} - result.once.Do(result.init) - - for k, v := range s.m { - if _, ok := other.m[k]; ok { - result.m[k] = v - } - } - - return result -} - -// Union performs the set union of the two sets and returns a new third -// set. -func (s *Set) Union(other *Set) *Set { - result := &Set{F: s.F} - result.once.Do(result.init) - - for k, v := range s.m { - result.m[k] = v - } - for k, v := range other.m { - result.m[k] = v - } - - return result -} - -func (s *Set) Equal(raw interface{}) bool { - other, ok := raw.(*Set) - if !ok { - return false - } - - return reflect.DeepEqual(s.m, other.m) -} - -// HashEqual simply checks to the keys the top-level map to the keys in the -// other set's top-level map to see if they are equal. This obviously assumes -// you have a properly working hash function - use HashResource if in doubt. -func (s *Set) HashEqual(raw interface{}) bool { - other, ok := raw.(*Set) - if !ok { - return false - } - - ks1 := make([]string, 0) - ks2 := make([]string, 0) - - for k := range s.m { - ks1 = append(ks1, k) - } - for k := range other.m { - ks2 = append(ks2, k) - } - - sort.Strings(ks1) - sort.Strings(ks2) - - return reflect.DeepEqual(ks1, ks2) -} - -func (s *Set) GoString() string { - return fmt.Sprintf("*Set(%#v)", s.m) -} - -func (s *Set) init() { - s.m = make(map[string]interface{}) -} - -func (s *Set) add(item interface{}, computed bool) string { - s.once.Do(s.init) - - code := s.hash(item) - if computed { - code = "~" + code - - if isProto5() { - tmpCode := code - count := 0 - for _, exists := s.m[tmpCode]; exists; _, exists = s.m[tmpCode] { - count++ - tmpCode = fmt.Sprintf("%s%d", code, count) - } - code = tmpCode - } - } - - if _, ok := s.m[code]; !ok { - s.m[code] = item - } - - return code -} - -func (s *Set) hash(item interface{}) string { - code := s.F(item) - // Always return a nonnegative hashcode. - if code < 0 { - code = -code - } - return strconv.Itoa(code) -} - -func (s *Set) remove(item interface{}) string { - s.once.Do(s.init) - - code := s.hash(item) - delete(s.m, code) - - return code -} - -func (s *Set) index(item interface{}) int { - return sort.SearchStrings(s.listCode(), s.hash(item)) -} - -func (s *Set) listCode() []string { - // Sort the hash codes so the order of the list is deterministic - keys := make([]string, 0, len(s.m)) - for k := range s.m { - keys = append(keys, k) - } - sort.Sort(sort.StringSlice(keys)) - return keys -} diff --git a/helper/schema/set_test.go b/helper/schema/set_test.go deleted file mode 100644 index edeeb37a6..000000000 --- a/helper/schema/set_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package schema - -import ( - "reflect" - "testing" -) - -func TestSetAdd(t *testing.T) { - s := &Set{F: testSetInt} - s.Add(1) - s.Add(5) - s.Add(25) - - expected := []interface{}{1, 25, 5} - actual := s.List() - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestSetAdd_negative(t *testing.T) { - // Since we don't allow negative hashes, this should just hash to the - // same thing... - s := &Set{F: testSetInt} - s.Add(-1) - s.Add(1) - - expected := []interface{}{-1} - actual := s.List() - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestSetContains(t *testing.T) { - s := &Set{F: testSetInt} - s.Add(5) - s.Add(-5) - - if s.Contains(2) { - t.Fatal("should not contain") - } - if !s.Contains(5) { - t.Fatal("should contain") - } - if !s.Contains(-5) { - t.Fatal("should contain") - } -} - -func TestSetDifference(t *testing.T) { - s1 := &Set{F: testSetInt} - s2 := &Set{F: testSetInt} - - s1.Add(1) - s1.Add(5) - - s2.Add(5) - s2.Add(25) - - difference := s1.Difference(s2) - difference.Add(2) - - expected := []interface{}{1, 2} - actual := difference.List() - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestSetIntersection(t *testing.T) { - s1 := &Set{F: testSetInt} - s2 := &Set{F: testSetInt} - - s1.Add(1) - s1.Add(5) - - s2.Add(5) - s2.Add(25) - - intersection := s1.Intersection(s2) - intersection.Add(2) - - expected := []interface{}{2, 5} - actual := intersection.List() - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func TestSetUnion(t *testing.T) { - s1 := &Set{F: testSetInt} - s2 := &Set{F: testSetInt} - - s1.Add(1) - s1.Add(5) - - s2.Add(5) - s2.Add(25) - - union := s1.Union(s2) - union.Add(2) - - expected := []interface{}{1, 2, 25, 5} - actual := union.List() - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } -} - -func testSetInt(v interface{}) int { - return v.(int) -} - -func TestHashResource_nil(t *testing.T) { - resource := &Resource{ - Schema: map[string]*Schema{ - "name": { - Type: TypeString, - Optional: true, - }, - }, - } - f := HashResource(resource) - - idx := f(nil) - if idx != 0 { - t.Fatalf("Expected 0 when hashing nil, given: %d", idx) - } -} - -func TestHashEqual(t *testing.T) { - nested := &Resource{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeString, - Optional: true, - }, - }, - } - root := &Resource{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeString, - Optional: true, - }, - "nested": { - Type: TypeSet, - Optional: true, - Elem: nested, - }, - }, - } - n1 := map[string]interface{}{"foo": "bar"} - n2 := map[string]interface{}{"foo": "baz"} - - r1 := map[string]interface{}{ - "bar": "baz", - "nested": NewSet(HashResource(nested), []interface{}{n1}), - } - r2 := map[string]interface{}{ - "bar": "qux", - "nested": NewSet(HashResource(nested), []interface{}{n2}), - } - r3 := map[string]interface{}{ - "bar": "baz", - "nested": NewSet(HashResource(nested), []interface{}{n2}), - } - r4 := map[string]interface{}{ - "bar": "qux", - "nested": NewSet(HashResource(nested), []interface{}{n1}), - } - s1 := NewSet(HashResource(root), []interface{}{r1}) - s2 := NewSet(HashResource(root), []interface{}{r2}) - s3 := NewSet(HashResource(root), []interface{}{r3}) - s4 := NewSet(HashResource(root), []interface{}{r4}) - - cases := []struct { - name string - set *Set - compare *Set - expected bool - }{ - { - name: "equal", - set: s1, - compare: s1, - expected: true, - }, - { - name: "not equal", - set: s1, - compare: s2, - expected: false, - }, - { - name: "outer equal, should still not be equal", - set: s1, - compare: s3, - expected: false, - }, - { - name: "inner equal, should still not be equal", - set: s1, - compare: s4, - expected: false, - }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - actual := tc.set.HashEqual(tc.compare) - if tc.expected != actual { - t.Fatalf("expected %t, got %t", tc.expected, actual) - } - }) - } -} diff --git a/helper/schema/shims.go b/helper/schema/shims.go deleted file mode 100644 index d2dbff53c..000000000 --- a/helper/schema/shims.go +++ /dev/null @@ -1,115 +0,0 @@ -package schema - -import ( - "encoding/json" - - "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" - - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/terraform" -) - -// DiffFromValues takes the current state and desired state as cty.Values and -// derives a terraform.InstanceDiff to give to the legacy providers. This is -// used to take the states provided by the new ApplyResourceChange method and -// convert them to a state+diff required for the legacy Apply method. -func DiffFromValues(prior, planned cty.Value, res *Resource) (*terraform.InstanceDiff, error) { - return diffFromValues(prior, planned, res, nil) -} - -// diffFromValues takes an additional CustomizeDiffFunc, so we can generate our -// test fixtures from the legacy tests. In the new provider protocol the diff -// only needs to be created for the apply operation, and any customizations -// have already been done. -func diffFromValues(prior, planned cty.Value, res *Resource, cust CustomizeDiffFunc) (*terraform.InstanceDiff, error) { - instanceState, err := res.ShimInstanceStateFromValue(prior) - if err != nil { - return nil, err - } - - configSchema := res.CoreConfigSchema() - - cfg := terraform.NewResourceConfigShimmed(planned, configSchema) - removeConfigUnknowns(cfg.Config) - removeConfigUnknowns(cfg.Raw) - - diff, err := schemaMap(res.Schema).Diff(instanceState, cfg, cust, nil, false) - if err != nil { - return nil, err - } - - return diff, err -} - -// During apply the only unknown values are those which are to be computed by -// the resource itself. These may have been marked as unknown config values, and -// need to be removed to prevent the UnknownVariableValue from appearing the diff. -func removeConfigUnknowns(cfg map[string]interface{}) { - for k, v := range cfg { - switch v := v.(type) { - case string: - if v == hcl2shim.UnknownVariableValue { - delete(cfg, k) - } - case []interface{}: - for _, i := range v { - if m, ok := i.(map[string]interface{}); ok { - removeConfigUnknowns(m) - } - } - case map[string]interface{}: - removeConfigUnknowns(v) - } - } -} - -// ApplyDiff takes a cty.Value state and applies a terraform.InstanceDiff to -// get a new cty.Value state. This is used to convert the diff returned from -// the legacy provider Diff method to the state required for the new -// PlanResourceChange method. -func ApplyDiff(base cty.Value, d *terraform.InstanceDiff, schema *configschema.Block) (cty.Value, error) { - return d.ApplyToValue(base, schema) -} - -// StateValueToJSONMap converts a cty.Value to generic JSON map via the cty JSON -// encoding. -func StateValueToJSONMap(val cty.Value, ty cty.Type) (map[string]interface{}, error) { - js, err := ctyjson.Marshal(val, ty) - if err != nil { - return nil, err - } - - var m map[string]interface{} - if err := json.Unmarshal(js, &m); err != nil { - return nil, err - } - - return m, nil -} - -// JSONMapToStateValue takes a generic json map[string]interface{} and converts it -// to the specific type, ensuring that the values conform to the schema. -func JSONMapToStateValue(m map[string]interface{}, block *configschema.Block) (cty.Value, error) { - var val cty.Value - - js, err := json.Marshal(m) - if err != nil { - return val, err - } - - val, err = ctyjson.Unmarshal(js, block.ImpliedType()) - if err != nil { - return val, err - } - - return block.CoerceValue(val) -} - -// StateValueFromInstanceState converts a terraform.InstanceState to a -// cty.Value as described by the provided cty.Type, and maintains the resource -// ID as the "id" attribute. -func StateValueFromInstanceState(is *terraform.InstanceState, ty cty.Type) (cty.Value, error) { - return is.AttrsAsObjectValue(ty) -} diff --git a/helper/schema/shims_test.go b/helper/schema/shims_test.go deleted file mode 100644 index 050286a02..000000000 --- a/helper/schema/shims_test.go +++ /dev/null @@ -1,3521 +0,0 @@ -package schema - -import ( - "bytes" - "errors" - "fmt" - "reflect" - "strconv" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/hashicorp/terraform/configs/configschema" - "github.com/hashicorp/terraform/configs/hcl2shim" - "github.com/hashicorp/terraform/helper/hashcode" - "github.com/hashicorp/terraform/providers" - "github.com/hashicorp/terraform/terraform" - "github.com/hashicorp/terraform/tfdiags" - "github.com/zclconf/go-cty/cty" -) - -var ( - typeComparer = cmp.Comparer(cty.Type.Equals) - valueComparer = cmp.Comparer(cty.Value.RawEquals) - equateEmpty = cmpopts.EquateEmpty() -) - -func testApplyDiff(t *testing.T, - resource *Resource, - state, expected *terraform.InstanceState, - diff *terraform.InstanceDiff) { - - testSchema := providers.Schema{ - Version: int64(resource.SchemaVersion), - Block: resourceSchemaToBlock(resource.Schema), - } - - stateVal, err := StateValueFromInstanceState(state, testSchema.Block.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - newState, err := ApplyDiff(stateVal, diff, testSchema.Block) - if err != nil { - t.Fatal(err) - } - - // verify that "id" is correct - id := newState.AsValueMap()["id"] - - switch { - case diff.Destroy || diff.DestroyDeposed || diff.DestroyTainted: - // there should be no id - if !id.IsNull() { - t.Fatalf("destroyed instance should have no id: %#v", id) - } - default: - // the "id" field always exists and is computed, so it must have a - // valid value or be unknown. - if id.IsNull() { - t.Fatal("new instance state cannot have a null id") - } - - if id.IsKnown() && id.AsString() == "" { - t.Fatal("new instance id cannot be an empty string") - } - } - - // Resource.Meta will be hanlded separately, so it's OK that we lose the - // timeout values here. - expectedState, err := StateValueFromInstanceState(expected, testSchema.Block.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - if !cmp.Equal(expectedState, newState, equateEmpty, typeComparer, valueComparer) { - t.Fatalf(cmp.Diff(expectedState, newState, equateEmpty, typeComparer, valueComparer)) - } -} - -func TestShimResourcePlan_destroyCreate(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - ForceNew: true, - }, - }, - } - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - RequiresNew: true, - Old: "3", - New: "42", - }, - }, - } - - state := &terraform.InstanceState{ - Attributes: map[string]string{"foo": "3"}, - } - - expected := &terraform.InstanceState{ - ID: hcl2shim.UnknownVariableValue, - Attributes: map[string]string{ - "id": hcl2shim.UnknownVariableValue, - "foo": "42", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - }, - } - - testApplyDiff(t, r, state, expected, d) -} - -func TestShimResourceApply_create(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - called := false - r.Create = func(d *ResourceData, m interface{}) error { - called = true - d.SetId("foo") - return nil - } - - var s *terraform.InstanceState = nil - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "42", - }, - }, - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !called { - t.Fatal("not called") - } - - expected := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "42", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) - } - - // Shim - // now that we have our diff and desired state, see if we can reproduce - // that with the shim - // we're not testing Resource.Create, so we need to start with the "created" state - createdState := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{"id": "foo"}, - } - - testApplyDiff(t, r, createdState, expected, d) -} - -func TestShimResourceApply_Timeout_state(t *testing.T) { - r := &Resource{ - SchemaVersion: 2, - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(40 * time.Minute), - Update: DefaultTimeout(80 * time.Minute), - Delete: DefaultTimeout(40 * time.Minute), - }, - } - - called := false - r.Create = func(d *ResourceData, m interface{}) error { - called = true - d.SetId("foo") - return nil - } - - var s *terraform.InstanceState = nil - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "42", - }, - }, - } - - diffTimeout := &ResourceTimeout{ - Create: DefaultTimeout(40 * time.Minute), - Update: DefaultTimeout(80 * time.Minute), - Delete: DefaultTimeout(40 * time.Minute), - } - - if err := diffTimeout.DiffEncode(d); err != nil { - t.Fatalf("Error encoding timeout to diff: %s", err) - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !called { - t.Fatal("not called") - } - - expected := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "42", - }, - Meta: map[string]interface{}{ - "schema_version": "2", - TimeoutKey: expectedForValues(40, 0, 80, 40, 0), - }, - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Not equal in Timeout State:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta) - } - - // Shim - // we're not testing Resource.Create, so we need to start with the "created" state - createdState := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{"id": "foo"}, - } - - testApplyDiff(t, r, createdState, expected, d) -} - -func TestShimResourceDiff_Timeout_diff(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(40 * time.Minute), - Update: DefaultTimeout(80 * time.Minute), - Delete: DefaultTimeout(40 * time.Minute), - }, - } - - r.Create = func(d *ResourceData, m interface{}) error { - d.SetId("foo") - return nil - } - - conf := terraform.NewResourceConfigRaw(map[string]interface{}{ - "foo": 42, - TimeoutsConfigKey: map[string]interface{}{ - "create": "2h", - }, - }) - var s *terraform.InstanceState - - actual, err := r.Diff(s, conf, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - New: "42", - }, - }, - } - - diffTimeout := &ResourceTimeout{ - Create: DefaultTimeout(120 * time.Minute), - Update: DefaultTimeout(80 * time.Minute), - Delete: DefaultTimeout(40 * time.Minute), - } - - if err := diffTimeout.DiffEncode(expected); err != nil { - t.Fatalf("Error encoding timeout to diff: %s", err) - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Not equal in Timeout Diff:\n\texpected: %#v\n\tactual: %#v", expected.Meta, actual.Meta) - } - - // Shim - // apply this diff, so we have a state to compare - applied, err := r.Apply(s, actual, nil) - if err != nil { - t.Fatal(err) - } - - // we're not testing Resource.Create, so we need to start with the "created" state - createdState := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{"id": "foo"}, - } - - testSchema := providers.Schema{ - Version: int64(r.SchemaVersion), - Block: resourceSchemaToBlock(r.Schema), - } - - initialVal, err := StateValueFromInstanceState(createdState, testSchema.Block.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - appliedVal, err := StateValueFromInstanceState(applied, testSchema.Block.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - d, err := DiffFromValues(initialVal, appliedVal, r) - if err != nil { - t.Fatal(err) - } - if eq, _ := d.Same(expected); !eq { - t.Fatal(cmp.Diff(d, expected)) - } -} - -func TestShimResourceApply_destroy(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - } - - called := false - r.Delete = func(d *ResourceData, m interface{}) error { - called = true - return nil - } - - s := &terraform.InstanceState{ - ID: "bar", - } - - d := &terraform.InstanceDiff{ - Destroy: true, - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !called { - t.Fatal("delete not called") - } - - if actual != nil { - t.Fatalf("bad: %#v", actual) - } - - // Shim - // now that we have our diff and desired state, see if we can reproduce - // that with the shim - testApplyDiff(t, r, s, actual, d) -} - -func TestShimResourceApply_destroyCreate(t *testing.T) { - r := &Resource{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - ForceNew: true, - }, - - "tags": &Schema{ - Type: TypeMap, - Optional: true, - Computed: true, - }, - }, - } - - change := false - r.Create = func(d *ResourceData, m interface{}) error { - change = d.HasChange("tags") - d.SetId("foo") - return nil - } - r.Delete = func(d *ResourceData, m interface{}) error { - return nil - } - - var s *terraform.InstanceState = &terraform.InstanceState{ - ID: "bar", - Attributes: map[string]string{ - "foo": "7", - "tags.Name": "foo", - }, - } - - d := &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "id": &terraform.ResourceAttrDiff{ - New: "foo", - }, - "foo": &terraform.ResourceAttrDiff{ - Old: "7", - New: "42", - RequiresNew: true, - }, - "tags.Name": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "foo", - RequiresNew: true, - }, - }, - } - - actual, err := r.Apply(s, d, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !change { - t.Fatal("should have change") - } - - expected := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "42", - "tags.%": "1", - "tags.Name": "foo", - }, - } - - if !reflect.DeepEqual(actual, expected) { - cmp.Diff(actual, expected) - } - - // Shim - // now that we have our diff and desired state, see if we can reproduce - // that with the shim - // we're not testing Resource.Create, so we need to start with the "created" state - createdState := &terraform.InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "id": "foo", - "foo": "7", - "tags.%": "1", - "tags.Name": "foo", - }, - } - - testApplyDiff(t, r, createdState, expected, d) -} - -func TestShimSchemaMap_Diff(t *testing.T) { - cases := []struct { - Name string - Schema map[string]*Schema - State *terraform.InstanceState - Config map[string]interface{} - CustomizeDiff CustomizeDiffFunc - Diff *terraform.InstanceDiff - Err bool - }{ - { - Name: "diff-1", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "diff-2", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "diff-3", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "foo", - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Computed, but set in config", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "availability_zone": "foo", - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "bar", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - }, - }, - - Err: false, - }, - - { - Name: "Default", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Default: "foo", - }, - }, - - State: nil, - - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - - Err: false, - }, - - { - Name: "DefaultFunc, value", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - DefaultFunc: func() (interface{}, error) { - return "foo", nil - }, - }, - }, - - State: nil, - - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - }, - }, - - Err: false, - }, - - { - Name: "DefaultFunc, configuration set", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - DefaultFunc: func() (interface{}, error) { - return "foo", nil - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "bar", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - - Err: false, - }, - - { - Name: "String with StateFunc", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - StateFunc: func(a interface{}) string { - return a.(string) + "!" - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo!", - NewExtra: "foo", - }, - }, - }, - - Err: false, - }, - - { - Name: "StateFunc not called with nil value", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - StateFunc: func(a interface{}) string { - t.Error("should not get here!") - return "" - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Variable computed", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": hcl2shim.UnknownVariableValue, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: hcl2shim.UnknownVariableValue, - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Int decode", - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeInt, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "port": 27, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "port": &terraform.ResourceAttrDiff{ - Old: "", - New: "27", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "bool decode", - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "port": false, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "port": &terraform.ResourceAttrDiff{ - Old: "", - New: "false", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Bool", - Schema: map[string]*Schema{ - "delete": &Schema{ - Type: TypeBool, - Optional: true, - Default: false, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "delete": "false", - }, - }, - - Config: nil, - - Diff: nil, - - Err: false, - }, - - { - Name: "List decode", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - }, - "ports.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "List decode with promotion with list", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - PromoteSingle: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{"5"}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "ports.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - }, - "ports.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, hcl2shim.UnknownVariableValue, "5"}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "ports.#": "3", - "ports.0": "1", - "ports.1": "2", - "ports.2": "5", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: nil, - - Err: false, - }, - - { - Name: "", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "ports.#": "2", - "ports.0": "1", - "ports.1": "2", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "3", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Required: true, - Elem: &Schema{Type: TypeInt}, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, 2, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - RequiresNew: true, - }, - "ports.0": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - RequiresNew: true, - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - RequiresNew: true, - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "List with computed set", - Schema: map[string]*Schema{ - "config": &Schema{ - Type: TypeList, - Optional: true, - ForceNew: true, - MinItems: 1, - Elem: &Resource{ - Schema: map[string]*Schema{ - "name": { - Type: TypeString, - Required: true, - }, - - "rules": { - Type: TypeSet, - Computed: true, - Elem: &Schema{Type: TypeString}, - Set: HashString, - }, - }, - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "config": []interface{}{ - map[string]interface{}{ - "name": "hello", - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - RequiresNew: true, - }, - - "config.0.name": &terraform.ResourceAttrDiff{ - Old: "", - New: "hello", - }, - - "config.0.rules.#": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-1", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{5, 2, 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-2", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Computed: true, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "ports.#": "0", - }, - }, - - Config: nil, - - Diff: nil, - - Err: false, - }, - - { - Name: "Set-3", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-4", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{"2", "5", 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "3", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-5", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ports": []interface{}{1, hcl2shim.UnknownVariableValue, 5}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-6", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "ports.#": "2", - "ports.1": "1", - "ports.2": "2", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{5, 2, 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "3", - }, - "ports.1": &terraform.ResourceAttrDiff{ - Old: "1", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "2", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-8", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "availability_zone": "bar", - "ports.#": "1", - "ports.80": "80", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Set-9", - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeSet, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeList, - Optional: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - ps := m["ports"].([]interface{}) - result := 0 - for _, p := range ps { - result += p.(int) - } - return result - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "ingress.#": "2", - "ingress.80.ports.#": "1", - "ingress.80.ports.0": "80", - "ingress.443.ports.#": "1", - "ingress.443.ports.0": "443", - }, - }, - - Config: map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "ports": []interface{}{443}, - }, - map[string]interface{}{ - "ports": []interface{}{80}, - }, - }, - }, - - Diff: nil, - - Err: false, - }, - - { - Name: "List of structure decode", - Schema: map[string]*Schema{ - "ingress": &Schema{ - Type: TypeList, - Required: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "from": &Schema{ - Type: TypeInt, - Required: true, - }, - }, - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ingress": []interface{}{ - map[string]interface{}{ - "from": 8080, - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ingress.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "ingress.0.from": &terraform.ResourceAttrDiff{ - Old: "", - New: "8080", - }, - }, - }, - - Err: false, - }, - - { - Name: "ComputedWhen", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Computed: true, - ComputedWhen: []string{"port"}, - }, - - "port": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "availability_zone": "foo", - "port": "80", - }, - }, - - Config: map[string]interface{}{ - "port": 80, - }, - - Diff: nil, - - Err: false, - }, - - { - Name: "computed", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Computed: true, - ComputedWhen: []string{"port"}, - }, - - "port": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "port": 80, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - "port": &terraform.ResourceAttrDiff{ - New: "80", - }, - }, - }, - - Err: false, - }, - - { - Name: "computed, exists", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Computed: true, - ComputedWhen: []string{"port"}, - }, - - "port": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "port": "80", - }, - }, - - Config: map[string]interface{}{ - "port": 80, - }, - - // there is no computed diff when the instance exists already - Diff: nil, - - Err: false, - }, - - { - Name: "Maps-1", - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeMap, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "config_vars": map[string]interface{}{ - "bar": "baz", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.%": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - - "config_vars.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - - Err: false, - }, - - { - Name: "Maps-2", - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeMap, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "config_vars.%": "1", - "config_vars.foo": "bar", - }, - }, - - Config: map[string]interface{}{ - "config_vars": map[string]interface{}{ - "bar": "baz", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.foo": &terraform.ResourceAttrDiff{ - Old: "bar", - NewRemoved: true, - }, - "config_vars.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - - Err: false, - }, - - { - Name: "Maps-3", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "vars.%": "1", - "vars.foo": "bar", - }, - }, - - Config: map[string]interface{}{ - "vars": map[string]interface{}{ - "bar": "baz", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "vars.foo": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "", - NewRemoved: true, - }, - "vars.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - - Err: false, - }, - - { - Name: "Maps-4", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "vars.%": "1", - "vars.foo": "bar", - }, - }, - - Config: nil, - - Diff: nil, - - Err: false, - }, - - { - Name: "Maps-5", - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeMap}, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "config_vars.#": "1", - "config_vars.0.%": "1", - "config_vars.0.foo": "bar", - }, - }, - - Config: map[string]interface{}{ - "config_vars": []interface{}{ - map[string]interface{}{ - "bar": "baz", - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.0.foo": &terraform.ResourceAttrDiff{ - Old: "bar", - NewRemoved: true, - }, - "config_vars.0.bar": &terraform.ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - }, - - Err: false, - }, - - { - Name: "Maps-6", - Schema: map[string]*Schema{ - "config_vars": &Schema{ - Type: TypeList, - Elem: &Schema{Type: TypeMap}, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "config_vars.#": "1", - "config_vars.0.%": "2", - "config_vars.0.foo": "bar", - "config_vars.0.bar": "baz", - }, - }, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config_vars.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "0", - }, - "config_vars.0.%": &terraform.ResourceAttrDiff{ - Old: "2", - New: "0", - }, - "config_vars.0.foo": &terraform.ResourceAttrDiff{ - Old: "bar", - NewRemoved: true, - }, - "config_vars.0.bar": &terraform.ResourceAttrDiff{ - Old: "baz", - NewRemoved: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "ForceNews", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - ForceNew: true, - }, - - "address": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "availability_zone": "bar", - "address": "foo", - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-10", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - ForceNew: true, - }, - - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "availability_zone": "bar", - "ports.#": "1", - "ports.80": "80", - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "bar", - New: "foo", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-11", - Schema: map[string]*Schema{ - "instances": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeString}, - Optional: true, - Computed: true, - Set: func(v interface{}) int { - return len(v.(string)) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "instances.#": "0", - }, - }, - - Config: map[string]interface{}{ - "instances": []interface{}{hcl2shim.UnknownVariableValue}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "instances.#": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-12", - Schema: map[string]*Schema{ - "route": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "gateway": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "route": []interface{}{ - map[string]interface{}{ - "index": "1", - "gateway": hcl2shim.UnknownVariableValue, - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "route.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "route.~1.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "route.~1.gateway": &terraform.ResourceAttrDiff{ - Old: "", - New: hcl2shim.UnknownVariableValue, - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set-13", - Schema: map[string]*Schema{ - "route": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "gateway": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "route": []interface{}{ - map[string]interface{}{ - "index": "1", - "gateway": []interface{}{ - hcl2shim.UnknownVariableValue, - }, - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "route.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "route.~1.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "route.~1.gateway.#": &terraform.ResourceAttrDiff{ - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Computed maps", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - Computed: true, - }, - }, - - State: nil, - - Config: nil, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "vars.%": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Computed maps", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "vars.%": "0", - }, - }, - - Config: map[string]interface{}{ - "vars": map[string]interface{}{ - "bar": hcl2shim.UnknownVariableValue, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "vars.%": &terraform.ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Empty", - Schema: map[string]*Schema{}, - - State: &terraform.InstanceState{}, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Float", - Schema: map[string]*Schema{ - "some_threshold": &Schema{ - Type: TypeFloat, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "some_threshold": "567.8", - }, - }, - - Config: map[string]interface{}{ - "some_threshold": 12.34, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "some_threshold": &terraform.ResourceAttrDiff{ - Old: "567.8", - New: "12.34", - }, - }, - }, - - Err: false, - }, - - { - Name: "https://github.com/hashicorp/terraform/issues/824", - Schema: map[string]*Schema{ - "block_device": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "device_name": &Schema{ - Type: TypeString, - Required: true, - }, - "delete_on_termination": &Schema{ - Type: TypeBool, - Optional: true, - Default: true, - }, - }, - }, - Set: func(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) - buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) - return hashcode.String(buf.String()) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "block_device.#": "2", - "block_device.616397234.delete_on_termination": "true", - "block_device.616397234.device_name": "/dev/sda1", - "block_device.2801811477.delete_on_termination": "true", - "block_device.2801811477.device_name": "/dev/sdx", - }, - }, - - Config: map[string]interface{}{ - "block_device": []interface{}{ - map[string]interface{}{ - "device_name": "/dev/sda1", - }, - map[string]interface{}{ - "device_name": "/dev/sdx", - }, - }, - }, - Diff: nil, - Err: false, - }, - - { - Name: "Zero value in state shouldn't result in diff", - Schema: map[string]*Schema{ - "port": &Schema{ - Type: TypeBool, - Optional: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "port": "false", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Same as prev, but for sets", - Schema: map[string]*Schema{ - "route": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "gateway": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "route.#": "0", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "A set computed element shouldn't cause a diff", - Schema: map[string]*Schema{ - "active": &Schema{ - Type: TypeBool, - Computed: true, - ForceNew: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "active": "true", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "An empty set should show up in the diff", - Schema: map[string]*Schema{ - "instances": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeString}, - Optional: true, - ForceNew: true, - Set: func(v interface{}) int { - return len(v.(string)) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "instances.#": "1", - "instances.3": "foo", - }, - }, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "instances.#": &terraform.ResourceAttrDiff{ - Old: "1", - New: "0", - RequiresNew: true, - }, - "instances.3": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "", - NewRemoved: true, - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Map with empty value", - Schema: map[string]*Schema{ - "vars": &Schema{ - Type: TypeMap, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "vars": map[string]interface{}{ - "foo": "", - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "vars.%": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "vars.foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "", - }, - }, - }, - - Err: false, - }, - - { - Name: "Unset bool, not in state", - Schema: map[string]*Schema{ - "force": &Schema{ - Type: TypeBool, - Optional: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Unset set, not in state", - Schema: map[string]*Schema{ - "metadata_keys": &Schema{ - Type: TypeSet, - Optional: true, - ForceNew: true, - Elem: &Schema{Type: TypeInt}, - Set: func(interface{}) int { return 0 }, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Unset list in state, should not show up computed", - Schema: map[string]*Schema{ - "metadata_keys": &Schema{ - Type: TypeList, - Optional: true, - Computed: true, - ForceNew: true, - Elem: &Schema{Type: TypeInt}, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "metadata_keys.#": "0", - }, - }, - - Config: map[string]interface{}{}, - - Diff: nil, - - Err: false, - }, - - { - Name: "Computed map without config that's known to be empty does not generate diff", - Schema: map[string]*Schema{ - "tags": &Schema{ - Type: TypeMap, - Computed: true, - }, - }, - - Config: nil, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "tags.%": "0", - }, - }, - - Diff: nil, - - Err: false, - }, - - { - Name: "Set with hyphen keys", - Schema: map[string]*Schema{ - "route": &Schema{ - Type: TypeSet, - Optional: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "index": &Schema{ - Type: TypeInt, - Required: true, - }, - - "gateway-name": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - }, - Set: func(v interface{}) int { - m := v.(map[string]interface{}) - return m["index"].(int) - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "route": []interface{}{ - map[string]interface{}{ - "index": "1", - "gateway-name": "hello", - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "route.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - }, - "route.1.index": &terraform.ResourceAttrDiff{ - Old: "", - New: "1", - }, - "route.1.gateway-name": &terraform.ResourceAttrDiff{ - Old: "", - New: "hello", - }, - }, - }, - - Err: false, - }, - - { - Name: "StateFunc in nested set (#1759)", - Schema: map[string]*Schema{ - "service_account": &Schema{ - Type: TypeList, - Optional: true, - ForceNew: true, - Elem: &Resource{ - Schema: map[string]*Schema{ - "scopes": &Schema{ - Type: TypeSet, - Required: true, - ForceNew: true, - Elem: &Schema{ - Type: TypeString, - StateFunc: func(v interface{}) string { - return v.(string) + "!" - }, - }, - Set: func(v interface{}) int { - i, err := strconv.Atoi(v.(string)) - if err != nil { - t.Fatalf("err: %s", err) - } - return i - }, - }, - }, - }, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "service_account": []interface{}{ - map[string]interface{}{ - "scopes": []interface{}{"123"}, - }, - }, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "service_account.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - RequiresNew: true, - }, - "service_account.0.scopes.#": &terraform.ResourceAttrDiff{ - Old: "0", - New: "1", - RequiresNew: true, - }, - "service_account.0.scopes.123": &terraform.ResourceAttrDiff{ - Old: "", - New: "123!", - NewExtra: "123", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Removing set elements", - Schema: map[string]*Schema{ - "instances": &Schema{ - Type: TypeSet, - Elem: &Schema{Type: TypeString}, - Optional: true, - ForceNew: true, - Set: func(v interface{}) int { - return len(v.(string)) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "instances.#": "2", - "instances.3": "333", - "instances.2": "22", - }, - }, - - Config: map[string]interface{}{ - "instances": []interface{}{"333", "4444"}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "instances.2": &terraform.ResourceAttrDiff{ - Old: "22", - New: "", - NewRemoved: true, - RequiresNew: true, - }, - "instances.3": &terraform.ResourceAttrDiff{ - Old: "333", - New: "333", - }, - "instances.4": &terraform.ResourceAttrDiff{ - Old: "", - New: "4444", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Bools can be set with 0/1 in config, still get true/false", - Schema: map[string]*Schema{ - "one": &Schema{ - Type: TypeBool, - Optional: true, - }, - "two": &Schema{ - Type: TypeBool, - Optional: true, - }, - "three": &Schema{ - Type: TypeBool, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "one": "false", - "two": "true", - "three": "true", - }, - }, - - Config: map[string]interface{}{ - "one": "1", - "two": "0", - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "one": &terraform.ResourceAttrDiff{ - Old: "false", - New: "true", - }, - "two": &terraform.ResourceAttrDiff{ - Old: "true", - New: "false", - }, - "three": &terraform.ResourceAttrDiff{ - Old: "true", - New: "false", - NewRemoved: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "tainted in state w/ no attr changes is still a replacement", - Schema: map[string]*Schema{}, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "id": "someid", - }, - Tainted: true, - }, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - DestroyTainted: true, - }, - }, - - { - Name: "Set ForceNew only marks the changing element as ForceNew", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - ForceNew: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "ports.#": "3", - "ports.1": "1", - "ports.2": "2", - "ports.4": "4", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{5, 2, 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.1": &terraform.ResourceAttrDiff{ - Old: "1", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "2", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - RequiresNew: true, - }, - "ports.4": &terraform.ResourceAttrDiff{ - Old: "4", - New: "0", - NewRemoved: true, - RequiresNew: true, - }, - }, - }, - }, - - { - Name: "removed optional items should trigger ForceNew", - Schema: map[string]*Schema{ - "description": &Schema{ - Type: TypeString, - ForceNew: true, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "description": "foo", - }, - }, - - Config: map[string]interface{}{}, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "description": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "", - RequiresNew: true, - NewRemoved: true, - }, - }, - }, - - Err: false, - }, - - // GH-7715 - { - Name: "computed value for boolean field", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeBool, - ForceNew: true, - Computed: true, - Optional: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - }, - - Config: map[string]interface{}{ - "foo": hcl2shim.UnknownVariableValue, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "foo": &terraform.ResourceAttrDiff{ - Old: "", - New: "false", - NewComputed: true, - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "Set ForceNew marks count as ForceNew if computed", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Required: true, - ForceNew: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "ports.#": "3", - "ports.1": "1", - "ports.2": "2", - "ports.4": "4", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{hcl2shim.UnknownVariableValue, 2, 1}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.#": &terraform.ResourceAttrDiff{ - NewComputed: true, - RequiresNew: true, - }, - }, - }, - }, - - { - Name: "List with computed schema and ForceNew", - Schema: map[string]*Schema{ - "config": &Schema{ - Type: TypeList, - Optional: true, - ForceNew: true, - Elem: &Schema{ - Type: TypeString, - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "config.#": "2", - "config.0": "a", - "config.1": "b", - }, - }, - - Config: map[string]interface{}{ - "config": []interface{}{hcl2shim.UnknownVariableValue, hcl2shim.UnknownVariableValue}, - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "config.#": &terraform.ResourceAttrDiff{ - Old: "2", - New: "", - RequiresNew: true, - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "overridden diff with a CustomizeDiff function, ForceNew not in schema", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("availability_zone", "bar"); err != nil { - return err - } - if err := d.ForceNew("availability_zone"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - // NOTE: This case is technically impossible in the current - // implementation, because optional+computed values never show up in the - // diff. In the event behavior changes this test should ensure that the - // intended diff still shows up. - Name: "overridden removed attribute diff with a CustomizeDiff function, ForceNew not in schema", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{}, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("availability_zone", "bar"); err != nil { - return err - } - if err := d.ForceNew("availability_zone"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - - Name: "overridden diff with a CustomizeDiff function, ForceNew in schema", - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "availability_zone": "foo", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("availability_zone", "bar"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "availability_zone": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "required field with computed diff added with CustomizeDiff function", - Schema: map[string]*Schema{ - "ami_id": &Schema{ - Type: TypeString, - Required: true, - }, - "instance_id": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - - State: nil, - - Config: map[string]interface{}{ - "ami_id": "foo", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("instance_id", "bar"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ami_id": &terraform.ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "instance_id": &terraform.ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - - Err: false, - }, - - { - Name: "Set ForceNew only marks the changing element as ForceNew - CustomizeDiffFunc edition", - Schema: map[string]*Schema{ - "ports": &Schema{ - Type: TypeSet, - Optional: true, - Computed: true, - Elem: &Schema{Type: TypeInt}, - Set: func(a interface{}) int { - return a.(int) - }, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "ports.#": "3", - "ports.1": "1", - "ports.2": "2", - "ports.4": "4", - }, - }, - - Config: map[string]interface{}{ - "ports": []interface{}{5, 2, 6}, - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if err := d.SetNew("ports", []interface{}{5, 2, 1}); err != nil { - return err - } - if err := d.ForceNew("ports"); err != nil { - return err - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "ports.1": &terraform.ResourceAttrDiff{ - Old: "1", - New: "1", - }, - "ports.2": &terraform.ResourceAttrDiff{ - Old: "2", - New: "2", - }, - "ports.5": &terraform.ResourceAttrDiff{ - Old: "", - New: "5", - RequiresNew: true, - }, - "ports.4": &terraform.ResourceAttrDiff{ - Old: "4", - New: "0", - NewRemoved: true, - RequiresNew: true, - }, - }, - }, - }, - - { - Name: "tainted resource does not run CustomizeDiffFunc", - Schema: map[string]*Schema{}, - - State: &terraform.InstanceState{ - ID: "someid", - Attributes: map[string]string{ - "id": "someid", - }, - Tainted: true, - }, - - Config: map[string]interface{}{}, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - return errors.New("diff customization should not have run") - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{}, - DestroyTainted: true, - }, - - Err: false, - }, - - { - Name: "NewComputed based on a conditional with CustomizeDiffFunc", - Schema: map[string]*Schema{ - "etag": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - "version_id": &Schema{ - Type: TypeString, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "etag": "foo", - "version_id": "1", - }, - }, - - Config: map[string]interface{}{ - "etag": "bar", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - if d.HasChange("etag") { - d.SetNewComputed("version_id") - } - return nil - }, - - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "etag": &terraform.ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - "version_id": &terraform.ResourceAttrDiff{ - Old: "1", - New: "", - NewComputed: true, - }, - }, - }, - - Err: false, - }, - - { - Name: "vetoing a diff", - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "foo": "bar", - }, - }, - - Config: map[string]interface{}{ - "foo": "baz", - }, - - CustomizeDiff: func(d *ResourceDiff, meta interface{}) error { - return fmt.Errorf("diff vetoed") - }, - - Err: true, - }, - - // A lot of resources currently depended on using the empty string as a - // nil/unset value. - { - Name: "optional, computed, empty string", - Schema: map[string]*Schema{ - "attr": &Schema{ - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "attr": "bar", - }, - }, - - Config: map[string]interface{}{ - "attr": "", - }, - }, - - { - Name: "optional, computed, empty string should not crash in CustomizeDiff", - Schema: map[string]*Schema{ - "unrelated_set": { - Type: TypeSet, - Optional: true, - Elem: &Schema{Type: TypeString}, - }, - "stream_enabled": { - Type: TypeBool, - Optional: true, - }, - "stream_view_type": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - - State: &terraform.InstanceState{ - ID: "id", - Attributes: map[string]string{ - "unrelated_set.#": "0", - "stream_enabled": "true", - "stream_view_type": "KEYS_ONLY", - }, - }, - Config: map[string]interface{}{ - "stream_enabled": false, - "stream_view_type": "", - }, - CustomizeDiff: func(diff *ResourceDiff, v interface{}) error { - v, ok := diff.GetOk("unrelated_set") - if ok { - return fmt.Errorf("Didn't expect unrelated_set: %#v", v) - } - return nil - }, - Diff: &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "stream_enabled": { - Old: "true", - New: "false", - }, - }, - }, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - c := terraform.NewResourceConfigRaw(tc.Config) - - { - d, err := schemaMap(tc.Schema).Diff(tc.State, c, tc.CustomizeDiff, nil, false) - if err != nil != tc.Err { - t.Fatalf("err: %s", err) - } - if !cmp.Equal(d, tc.Diff, equateEmpty) { - t.Fatal(cmp.Diff(d, tc.Diff, equateEmpty)) - } - } - // up to here is already tested in helper/schema; we're just - // verify that we haven't broken any tests in transition. - - // create a schema from the schemaMap - testSchema := resourceSchemaToBlock(tc.Schema) - - // get our initial state cty.Value - stateVal, err := StateValueFromInstanceState(tc.State, testSchema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - // this is the desired cty.Value from the configuration - configVal := hcl2shim.HCL2ValueFromConfigValue(c.Config) - - // verify that we can round-trip the config - origConfig := hcl2shim.ConfigValueFromHCL2(configVal) - if !cmp.Equal(c.Config, origConfig, equateEmpty) { - t.Fatal(cmp.Diff(c.Config, origConfig, equateEmpty)) - } - - // make sure our config conforms precisely to the schema - configVal, err = testSchema.CoerceValue(configVal) - if err != nil { - t.Fatal(tfdiags.FormatError(err)) - } - - // The new API requires returning the desired state rather than a - // diff, so we need to verify that we can combine the state and - // diff and recreate a new state. - - // now verify that we can create diff, using the new config and state values - // customize isn't run on tainted resources - tainted := tc.State != nil && tc.State.Tainted - if tainted { - tc.CustomizeDiff = nil - } - - res := &Resource{Schema: tc.Schema} - - d, err := diffFromValues(stateVal, configVal, res, tc.CustomizeDiff) - if err != nil { - if !tc.Err { - t.Fatal(err) - } - } - - // In a real "apply" operation there would be no unknown values, - // so for tests containing unknowns we'll stop here: the steps - // after this point apply only to the apply phase. - if !configVal.IsWhollyKnown() { - return - } - - // our diff function can't set DestroyTainted, but match the - // expected value here for the test fixtures - if tainted { - if d == nil { - d = &terraform.InstanceDiff{} - } - d.DestroyTainted = true - } - - if eq, _ := d.Same(tc.Diff); !eq { - t.Fatal(cmp.Diff(d, tc.Diff)) - } - - }) - } -} - -func resourceSchemaToBlock(s map[string]*Schema) *configschema.Block { - return (&Resource{Schema: s}).CoreConfigSchema() -} - -func TestRemoveConfigUnknowns(t *testing.T) { - cfg := map[string]interface{}{ - "id": "74D93920-ED26-11E3-AC10-0800200C9A66", - "route_rules": []interface{}{ - map[string]interface{}{ - "cidr_block": "74D93920-ED26-11E3-AC10-0800200C9A66", - "destination": "0.0.0.0/0", - "destination_type": "CIDR_BLOCK", - "network_entity_id": "1", - }, - map[string]interface{}{ - "cidr_block": "74D93920-ED26-11E3-AC10-0800200C9A66", - "destination": "0.0.0.0/0", - "destination_type": "CIDR_BLOCK", - "sub_block": []interface{}{ - map[string]interface{}{ - "computed": "74D93920-ED26-11E3-AC10-0800200C9A66", - }, - }, - }, - }, - } - - expect := map[string]interface{}{ - "route_rules": []interface{}{ - map[string]interface{}{ - "destination": "0.0.0.0/0", - "destination_type": "CIDR_BLOCK", - "network_entity_id": "1", - }, - map[string]interface{}{ - "destination": "0.0.0.0/0", - "destination_type": "CIDR_BLOCK", - "sub_block": []interface{}{ - map[string]interface{}{}, - }, - }, - }, - } - - removeConfigUnknowns(cfg) - - if !reflect.DeepEqual(cfg, expect) { - t.Fatalf("\nexpected: %#v\ngot: %#v", expect, cfg) - } -} diff --git a/helper/schema/testing.go b/helper/schema/testing.go deleted file mode 100644 index 122782174..000000000 --- a/helper/schema/testing.go +++ /dev/null @@ -1,28 +0,0 @@ -package schema - -import ( - "testing" - - "github.com/hashicorp/terraform/terraform" -) - -// TestResourceDataRaw creates a ResourceData from a raw configuration map. -func TestResourceDataRaw( - t *testing.T, schema map[string]*Schema, raw map[string]interface{}) *ResourceData { - t.Helper() - - c := terraform.NewResourceConfigRaw(raw) - - sm := schemaMap(schema) - diff, err := sm.Diff(nil, c, nil, nil, true) - if err != nil { - t.Fatalf("err: %s", err) - } - - result, err := sm.Data(nil, diff) - if err != nil { - t.Fatalf("err: %s", err) - } - - return result -} diff --git a/helper/schema/valuetype.go b/helper/schema/valuetype.go deleted file mode 100644 index 0f65d692f..000000000 --- a/helper/schema/valuetype.go +++ /dev/null @@ -1,21 +0,0 @@ -package schema - -//go:generate go run golang.org/x/tools/cmd/stringer -type=ValueType valuetype.go - -// ValueType is an enum of the type that can be represented by a schema. -type ValueType int - -const ( - TypeInvalid ValueType = iota - TypeBool - TypeInt - TypeFloat - TypeString - TypeList - TypeMap - TypeSet - typeObject -) - -// NOTE: ValueType has more functions defined on it in schema.go. We can't -// put them here because we reference other files. diff --git a/helper/schema/valuetype_string.go b/helper/schema/valuetype_string.go deleted file mode 100644 index 914ca32cb..000000000 --- a/helper/schema/valuetype_string.go +++ /dev/null @@ -1,31 +0,0 @@ -// Code generated by "stringer -type=ValueType valuetype.go"; DO NOT EDIT. - -package schema - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[TypeInvalid-0] - _ = x[TypeBool-1] - _ = x[TypeInt-2] - _ = x[TypeFloat-3] - _ = x[TypeString-4] - _ = x[TypeList-5] - _ = x[TypeMap-6] - _ = x[TypeSet-7] - _ = x[typeObject-8] -} - -const _ValueType_name = "TypeInvalidTypeBoolTypeIntTypeFloatTypeStringTypeListTypeMapTypeSettypeObject" - -var _ValueType_index = [...]uint8{0, 11, 19, 26, 35, 45, 53, 60, 67, 77} - -func (i ValueType) String() string { - if i < 0 || i >= ValueType(len(_ValueType_index)-1) { - return "ValueType(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _ValueType_name[_ValueType_index[i]:_ValueType_index[i+1]] -} diff --git a/helper/validation/validation.go b/helper/validation/validation.go deleted file mode 100644 index 484f7d7da..000000000 --- a/helper/validation/validation.go +++ /dev/null @@ -1,49 +0,0 @@ -package validation - -import ( - "fmt" - "strings" - - "github.com/hashicorp/terraform/helper/schema" -) - -// IntBetween returns a SchemaValidateFunc which tests if the provided value -// is of type int and is between min and max (inclusive) -func IntBetween(min, max int) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(int) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be int", k)) - return - } - - if v < min || v > max { - es = append(es, fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)) - return - } - - return - } -} - -// StringInSlice returns a SchemaValidateFunc which tests if the provided value -// is of type string and matches the value of an element in the valid slice -// will test with in lower case if ignoreCase is true -func StringInSlice(valid []string, ignoreCase bool) schema.SchemaValidateFunc { - return func(i interface{}, k string) (s []string, es []error) { - v, ok := i.(string) - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be string", k)) - return - } - - for _, str := range valid { - if v == str || (ignoreCase && strings.ToLower(v) == strings.ToLower(str)) { - return - } - } - - es = append(es, fmt.Errorf("expected %s to be one of %v, got %s", k, valid, v)) - return - } -} diff --git a/helper/validation/validation_test.go b/helper/validation/validation_test.go deleted file mode 100644 index b47fe7824..000000000 --- a/helper/validation/validation_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package validation - -import ( - "regexp" - "testing" - - "github.com/hashicorp/terraform/helper/schema" -) - -type testCase struct { - val interface{} - f schema.SchemaValidateFunc - expectedErr *regexp.Regexp -} - -func TestValidationIntBetween(t *testing.T) { - runTestCases(t, []testCase{ - { - val: 1, - f: IntBetween(1, 1), - }, - { - val: 1, - f: IntBetween(0, 2), - }, - { - val: 1, - f: IntBetween(2, 3), - expectedErr: regexp.MustCompile("expected [\\w]+ to be in the range \\(2 - 3\\), got 1"), - }, - { - val: "1", - f: IntBetween(2, 3), - expectedErr: regexp.MustCompile("expected type of [\\w]+ to be int"), - }, - }) -} - -func TestValidationStringInSlice(t *testing.T) { - runTestCases(t, []testCase{ - { - val: "ValidValue", - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), - }, - // ignore case - { - val: "VALIDVALUE", - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, true), - }, - { - val: "VALIDVALUE", - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), - expectedErr: regexp.MustCompile("expected [\\w]+ to be one of \\[ValidValue AnotherValidValue\\], got VALIDVALUE"), - }, - { - val: "InvalidValue", - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), - expectedErr: regexp.MustCompile("expected [\\w]+ to be one of \\[ValidValue AnotherValidValue\\], got InvalidValue"), - }, - { - val: 1, - f: StringInSlice([]string{"ValidValue", "AnotherValidValue"}, false), - expectedErr: regexp.MustCompile("expected type of [\\w]+ to be string"), - }, - }) -} - -func runTestCases(t *testing.T, cases []testCase) { - matchErr := func(errs []error, r *regexp.Regexp) bool { - // err must match one provided - for _, err := range errs { - if r.MatchString(err.Error()) { - return true - } - } - - return false - } - - for i, tc := range cases { - _, errs := tc.f(tc.val, "test_property") - - if len(errs) == 0 && tc.expectedErr == nil { - continue - } - - if len(errs) != 0 && tc.expectedErr == nil { - t.Fatalf("expected test case %d to produce no errors, got %v", i, errs) - } - - if !matchErr(errs, tc.expectedErr) { - t.Fatalf("expected test case %d to produce error matching \"%s\", got %v", i, tc.expectedErr, errs) - } - } -}