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 }