github.com/opentofu/opentofu@v1.7.1/internal/builtin/providers/tf/data_source_state_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tf
     7  
     8  import (
     9  	"fmt"
    10  	"log"
    11  	"testing"
    12  
    13  	"github.com/apparentlymart/go-dump/dump"
    14  	"github.com/opentofu/opentofu/internal/backend"
    15  	"github.com/opentofu/opentofu/internal/configs/configschema"
    16  	"github.com/opentofu/opentofu/internal/encryption"
    17  	"github.com/opentofu/opentofu/internal/states/statemgr"
    18  	"github.com/opentofu/opentofu/internal/tfdiags"
    19  	"github.com/zclconf/go-cty/cty"
    20  )
    21  
    22  func TestResource(t *testing.T) {
    23  	if err := dataSourceRemoteStateGetSchema().Block.InternalValidate(); err != nil {
    24  		t.Fatalf("err: %s", err)
    25  	}
    26  }
    27  
    28  func TestState_basic(t *testing.T) {
    29  	var tests = map[string]struct {
    30  		Config cty.Value
    31  		Want   cty.Value
    32  		Err    bool
    33  	}{
    34  		"basic": {
    35  			cty.ObjectVal(map[string]cty.Value{
    36  				"backend": cty.StringVal("local"),
    37  				"config": cty.ObjectVal(map[string]cty.Value{
    38  					"path": cty.StringVal("./testdata/basic.tfstate"),
    39  				}),
    40  			}),
    41  			cty.ObjectVal(map[string]cty.Value{
    42  				"backend": cty.StringVal("local"),
    43  				"config": cty.ObjectVal(map[string]cty.Value{
    44  					"path": cty.StringVal("./testdata/basic.tfstate"),
    45  				}),
    46  				"outputs": cty.ObjectVal(map[string]cty.Value{
    47  					"foo": cty.StringVal("bar"),
    48  				}),
    49  				"defaults":  cty.NullVal(cty.DynamicPseudoType),
    50  				"workspace": cty.NullVal(cty.String),
    51  			}),
    52  			false,
    53  		},
    54  		"workspace": {
    55  			cty.ObjectVal(map[string]cty.Value{
    56  				"backend":   cty.StringVal("local"),
    57  				"workspace": cty.StringVal(backend.DefaultStateName),
    58  				"config": cty.ObjectVal(map[string]cty.Value{
    59  					"path": cty.StringVal("./testdata/basic.tfstate"),
    60  				}),
    61  			}),
    62  			cty.ObjectVal(map[string]cty.Value{
    63  				"backend":   cty.StringVal("local"),
    64  				"workspace": cty.StringVal(backend.DefaultStateName),
    65  				"config": cty.ObjectVal(map[string]cty.Value{
    66  					"path": cty.StringVal("./testdata/basic.tfstate"),
    67  				}),
    68  				"outputs": cty.ObjectVal(map[string]cty.Value{
    69  					"foo": cty.StringVal("bar"),
    70  				}),
    71  				"defaults": cty.NullVal(cty.DynamicPseudoType),
    72  			}),
    73  			false,
    74  		},
    75  		"_local": {
    76  			cty.ObjectVal(map[string]cty.Value{
    77  				"backend": cty.StringVal("_local"),
    78  				"config": cty.ObjectVal(map[string]cty.Value{
    79  					"path": cty.StringVal("./testdata/basic.tfstate"),
    80  				}),
    81  			}),
    82  			cty.ObjectVal(map[string]cty.Value{
    83  				"backend": cty.StringVal("_local"),
    84  				"config": cty.ObjectVal(map[string]cty.Value{
    85  					"path": cty.StringVal("./testdata/basic.tfstate"),
    86  				}),
    87  				"outputs": cty.ObjectVal(map[string]cty.Value{
    88  					"foo": cty.StringVal("bar"),
    89  				}),
    90  				"defaults":  cty.NullVal(cty.DynamicPseudoType),
    91  				"workspace": cty.NullVal(cty.String),
    92  			}),
    93  			false,
    94  		},
    95  		"complex outputs": {
    96  			cty.ObjectVal(map[string]cty.Value{
    97  				"backend": cty.StringVal("local"),
    98  				"config": cty.ObjectVal(map[string]cty.Value{
    99  					"path": cty.StringVal("./testdata/complex_outputs.tfstate"),
   100  				}),
   101  			}),
   102  			cty.ObjectVal(map[string]cty.Value{
   103  				"backend": cty.StringVal("local"),
   104  				"config": cty.ObjectVal(map[string]cty.Value{
   105  					"path": cty.StringVal("./testdata/complex_outputs.tfstate"),
   106  				}),
   107  				"outputs": cty.ObjectVal(map[string]cty.Value{
   108  					"computed_map": cty.MapVal(map[string]cty.Value{
   109  						"key1": cty.StringVal("value1"),
   110  					}),
   111  					"computed_set": cty.ListVal([]cty.Value{
   112  						cty.StringVal("setval1"),
   113  						cty.StringVal("setval2"),
   114  					}),
   115  					"map": cty.MapVal(map[string]cty.Value{
   116  						"key":  cty.StringVal("test"),
   117  						"test": cty.StringVal("test"),
   118  					}),
   119  					"set": cty.ListVal([]cty.Value{
   120  						cty.StringVal("test1"),
   121  						cty.StringVal("test2"),
   122  					}),
   123  				}),
   124  				"defaults":  cty.NullVal(cty.DynamicPseudoType),
   125  				"workspace": cty.NullVal(cty.String),
   126  			}),
   127  			false,
   128  		},
   129  		"null outputs": {
   130  			cty.ObjectVal(map[string]cty.Value{
   131  				"backend": cty.StringVal("local"),
   132  				"config": cty.ObjectVal(map[string]cty.Value{
   133  					"path": cty.StringVal("./testdata/null_outputs.tfstate"),
   134  				}),
   135  			}),
   136  			cty.ObjectVal(map[string]cty.Value{
   137  				"backend": cty.StringVal("local"),
   138  				"config": cty.ObjectVal(map[string]cty.Value{
   139  					"path": cty.StringVal("./testdata/null_outputs.tfstate"),
   140  				}),
   141  				"outputs": cty.ObjectVal(map[string]cty.Value{
   142  					"map":  cty.NullVal(cty.Map(cty.String)),
   143  					"list": cty.NullVal(cty.List(cty.String)),
   144  				}),
   145  				"defaults":  cty.NullVal(cty.DynamicPseudoType),
   146  				"workspace": cty.NullVal(cty.String),
   147  			}),
   148  			false,
   149  		},
   150  		"defaults": {
   151  			cty.ObjectVal(map[string]cty.Value{
   152  				"backend": cty.StringVal("local"),
   153  				"config": cty.ObjectVal(map[string]cty.Value{
   154  					"path": cty.StringVal("./testdata/empty.tfstate"),
   155  				}),
   156  				"defaults": cty.ObjectVal(map[string]cty.Value{
   157  					"foo": cty.StringVal("bar"),
   158  				}),
   159  			}),
   160  			cty.ObjectVal(map[string]cty.Value{
   161  				"backend": cty.StringVal("local"),
   162  				"config": cty.ObjectVal(map[string]cty.Value{
   163  					"path": cty.StringVal("./testdata/empty.tfstate"),
   164  				}),
   165  				"defaults": cty.ObjectVal(map[string]cty.Value{
   166  					"foo": cty.StringVal("bar"),
   167  				}),
   168  				"outputs": cty.ObjectVal(map[string]cty.Value{
   169  					"foo": cty.StringVal("bar"),
   170  				}),
   171  				"workspace": cty.NullVal(cty.String),
   172  			}),
   173  			false,
   174  		},
   175  		"missing": {
   176  			cty.ObjectVal(map[string]cty.Value{
   177  				"backend": cty.StringVal("local"),
   178  				"config": cty.ObjectVal(map[string]cty.Value{
   179  					"path": cty.StringVal("./testdata/missing.tfstate"), // intentionally not present on disk
   180  				}),
   181  			}),
   182  			cty.ObjectVal(map[string]cty.Value{
   183  				"backend": cty.StringVal("local"),
   184  				"config": cty.ObjectVal(map[string]cty.Value{
   185  					"path": cty.StringVal("./testdata/missing.tfstate"),
   186  				}),
   187  				"defaults":  cty.NullVal(cty.DynamicPseudoType),
   188  				"outputs":   cty.EmptyObjectVal,
   189  				"workspace": cty.NullVal(cty.String),
   190  			}),
   191  			true,
   192  		},
   193  		"wrong type for config": {
   194  			cty.ObjectVal(map[string]cty.Value{
   195  				"backend": cty.StringVal("local"),
   196  				"config":  cty.StringVal("nope"),
   197  			}),
   198  			cty.NilVal,
   199  			true,
   200  		},
   201  		"wrong type for config with unknown backend": {
   202  			cty.ObjectVal(map[string]cty.Value{
   203  				"backend": cty.UnknownVal(cty.String),
   204  				"config":  cty.StringVal("nope"),
   205  			}),
   206  			cty.NilVal,
   207  			true,
   208  		},
   209  		"wrong type for config with unknown config": {
   210  			cty.ObjectVal(map[string]cty.Value{
   211  				"backend": cty.StringVal("local"),
   212  				"config":  cty.UnknownVal(cty.String),
   213  			}),
   214  			cty.NilVal,
   215  			true,
   216  		},
   217  		"wrong type for defaults": {
   218  			cty.ObjectVal(map[string]cty.Value{
   219  				"backend": cty.StringVal("local"),
   220  				"config": cty.ObjectVal(map[string]cty.Value{
   221  					"path": cty.StringVal("./testdata/basic.tfstate"),
   222  				}),
   223  				"defaults": cty.StringVal("nope"),
   224  			}),
   225  			cty.NilVal,
   226  			true,
   227  		},
   228  		"config as map": {
   229  			cty.ObjectVal(map[string]cty.Value{
   230  				"backend": cty.StringVal("local"),
   231  				"config": cty.MapVal(map[string]cty.Value{
   232  					"path": cty.StringVal("./testdata/empty.tfstate"),
   233  				}),
   234  			}),
   235  			cty.ObjectVal(map[string]cty.Value{
   236  				"backend": cty.StringVal("local"),
   237  				"config": cty.MapVal(map[string]cty.Value{
   238  					"path": cty.StringVal("./testdata/empty.tfstate"),
   239  				}),
   240  				"defaults":  cty.NullVal(cty.DynamicPseudoType),
   241  				"outputs":   cty.EmptyObjectVal,
   242  				"workspace": cty.NullVal(cty.String),
   243  			}),
   244  			false,
   245  		},
   246  		"defaults as map": {
   247  			cty.ObjectVal(map[string]cty.Value{
   248  				"backend": cty.StringVal("local"),
   249  				"config": cty.ObjectVal(map[string]cty.Value{
   250  					"path": cty.StringVal("./testdata/basic.tfstate"),
   251  				}),
   252  				"defaults": cty.MapValEmpty(cty.String),
   253  			}),
   254  			cty.ObjectVal(map[string]cty.Value{
   255  				"backend": cty.StringVal("local"),
   256  				"config": cty.ObjectVal(map[string]cty.Value{
   257  					"path": cty.StringVal("./testdata/basic.tfstate"),
   258  				}),
   259  				"defaults": cty.MapValEmpty(cty.String),
   260  				"outputs": cty.ObjectVal(map[string]cty.Value{
   261  					"foo": cty.StringVal("bar"),
   262  				}),
   263  				"workspace": cty.NullVal(cty.String),
   264  			}),
   265  			false,
   266  		},
   267  		"nonexistent backend": {
   268  			cty.ObjectVal(map[string]cty.Value{
   269  				"backend": cty.StringVal("nonexistent"),
   270  				"config": cty.ObjectVal(map[string]cty.Value{
   271  					"path": cty.StringVal("./testdata/basic.tfstate"),
   272  				}),
   273  			}),
   274  			cty.NilVal,
   275  			true,
   276  		},
   277  		"null config": {
   278  			cty.ObjectVal(map[string]cty.Value{
   279  				"backend": cty.StringVal("local"),
   280  				"config":  cty.NullVal(cty.DynamicPseudoType),
   281  			}),
   282  			cty.NilVal,
   283  			true,
   284  		},
   285  	}
   286  	for name, test := range tests {
   287  		t.Run(name, func(t *testing.T) {
   288  			schema := dataSourceRemoteStateGetSchema().Block
   289  			config, err := schema.CoerceValue(test.Config)
   290  			if err != nil {
   291  				t.Fatalf("unexpected error: %s", err)
   292  			}
   293  
   294  			diags := dataSourceRemoteStateValidate(config)
   295  
   296  			var got cty.Value
   297  			if !diags.HasErrors() && config.IsWhollyKnown() {
   298  				var moreDiags tfdiags.Diagnostics
   299  				got, moreDiags = dataSourceRemoteStateRead(config, encryption.StateEncryptionDisabled())
   300  				diags = diags.Append(moreDiags)
   301  			}
   302  
   303  			if test.Err {
   304  				if !diags.HasErrors() {
   305  					t.Fatal("succeeded; want error")
   306  				}
   307  			} else if diags.HasErrors() {
   308  				t.Fatalf("unexpected errors: %s", diags.Err())
   309  			}
   310  
   311  			if test.Want != cty.NilVal && !test.Want.RawEquals(got) {
   312  				t.Errorf("wrong result\nconfig: %sgot:    %swant:   %s", dump.Value(config), dump.Value(got), dump.Value(test.Want))
   313  			}
   314  		})
   315  	}
   316  }
   317  
   318  func TestState_validation(t *testing.T) {
   319  	// The main test TestState_basic covers both validation and reading of
   320  	// state snapshots, so this additional test is here only to verify that
   321  	// the validation step in isolation does not attempt to configure
   322  	// the backend.
   323  	overrideBackendFactories = map[string]backend.InitFn{
   324  		"failsconfigure": func(enc encryption.StateEncryption) backend.Backend {
   325  			return backendFailsConfigure{}
   326  		},
   327  	}
   328  	defer func() {
   329  		// undo our overrides so we won't affect other tests
   330  		overrideBackendFactories = nil
   331  	}()
   332  
   333  	schema := dataSourceRemoteStateGetSchema().Block
   334  	config, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
   335  		"backend": cty.StringVal("failsconfigure"),
   336  		"config":  cty.EmptyObjectVal,
   337  	}))
   338  	if err != nil {
   339  		t.Fatalf("unexpected error: %s", err)
   340  	}
   341  
   342  	diags := dataSourceRemoteStateValidate(config)
   343  	if diags.HasErrors() {
   344  		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
   345  	}
   346  }
   347  
   348  type backendFailsConfigure struct{}
   349  
   350  func (b backendFailsConfigure) ConfigSchema() *configschema.Block {
   351  	log.Printf("[TRACE] backendFailsConfigure.ConfigSchema")
   352  	return &configschema.Block{} // intentionally empty configuration schema
   353  }
   354  
   355  func (b backendFailsConfigure) PrepareConfig(given cty.Value) (cty.Value, tfdiags.Diagnostics) {
   356  	// No special actions to take here
   357  	return given, nil
   358  }
   359  
   360  func (b backendFailsConfigure) Configure(config cty.Value) tfdiags.Diagnostics {
   361  	log.Printf("[TRACE] backendFailsConfigure.Configure(%#v)", config)
   362  	var diags tfdiags.Diagnostics
   363  	diags = diags.Append(fmt.Errorf("Configure should never be called"))
   364  	return diags
   365  }
   366  
   367  func (b backendFailsConfigure) StateMgr(workspace string) (statemgr.Full, error) {
   368  	return nil, fmt.Errorf("StateMgr not implemented")
   369  }
   370  
   371  func (b backendFailsConfigure) DeleteWorkspace(name string, _ bool) error {
   372  	return fmt.Errorf("DeleteWorkspace not implemented")
   373  }
   374  
   375  func (b backendFailsConfigure) Workspaces() ([]string, error) {
   376  	return nil, fmt.Errorf("Workspaces not implemented")
   377  }