github.com/opentofu/opentofu@v1.7.1/internal/tofu/context_input_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 tofu 7 8 import ( 9 "reflect" 10 "strings" 11 "sync" 12 "testing" 13 14 "github.com/zclconf/go-cty/cty" 15 16 "github.com/opentofu/opentofu/internal/addrs" 17 "github.com/opentofu/opentofu/internal/configs/configschema" 18 "github.com/opentofu/opentofu/internal/plans" 19 "github.com/opentofu/opentofu/internal/providers" 20 "github.com/opentofu/opentofu/internal/states" 21 ) 22 23 func TestContext2Input_provider(t *testing.T) { 24 m := testModule(t, "input-provider") 25 p := testProvider("aws") 26 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 27 Provider: &configschema.Block{ 28 Attributes: map[string]*configschema.Attribute{ 29 "foo": { 30 Type: cty.String, 31 Required: true, 32 Description: "something something", 33 }, 34 }, 35 }, 36 ResourceTypes: map[string]*configschema.Block{ 37 "aws_instance": { 38 Attributes: map[string]*configschema.Attribute{ 39 "id": { 40 Type: cty.String, 41 Computed: true, 42 }, 43 }, 44 }, 45 }, 46 }) 47 48 inp := &MockUIInput{ 49 InputReturnMap: map[string]string{ 50 "provider.aws.foo": "bar", 51 }, 52 } 53 54 ctx := testContext2(t, &ContextOpts{ 55 Providers: map[addrs.Provider]providers.Factory{ 56 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 57 }, 58 UIInput: inp, 59 }) 60 61 var actual interface{} 62 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 63 actual = req.Config.GetAttr("foo").AsString() 64 return 65 } 66 67 if diags := ctx.Input(m, InputModeStd); diags.HasErrors() { 68 t.Fatalf("input errors: %s", diags.Err()) 69 } 70 71 if !inp.InputCalled { 72 t.Fatal("no input prompt; want prompt for argument \"foo\"") 73 } 74 if got, want := inp.InputOpts.Description, "something something"; got != want { 75 t.Errorf("wrong description\ngot: %q\nwant: %q", got, want) 76 } 77 78 plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 79 assertNoErrors(t, diags) 80 81 if _, diags := ctx.Apply(plan, m); diags.HasErrors() { 82 t.Fatalf("apply errors: %s", diags.Err()) 83 } 84 85 if !reflect.DeepEqual(actual, "bar") { 86 t.Fatalf("wrong result\ngot: %#v\nwant: %#v", actual, "bar") 87 } 88 } 89 90 func TestContext2Input_providerMulti(t *testing.T) { 91 m := testModule(t, "input-provider-multi") 92 93 getProviderSchemaResponse := getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 94 Provider: &configschema.Block{ 95 Attributes: map[string]*configschema.Attribute{ 96 "foo": { 97 Type: cty.String, 98 Required: true, 99 Description: "something something", 100 }, 101 }, 102 }, 103 ResourceTypes: map[string]*configschema.Block{ 104 "aws_instance": { 105 Attributes: map[string]*configschema.Attribute{ 106 "id": { 107 Type: cty.String, 108 Computed: true, 109 }, 110 }, 111 }, 112 }, 113 }) 114 115 // In order to update the provider to check only the configure calls during 116 // apply, we will need to inject a new factory function after plan. We must 117 // use a closure around the factory, because in order for the inputs to 118 // work during apply we need to maintain the same context value, preventing 119 // us from assigning a new Providers map. 120 providerFactory := func() (providers.Interface, error) { 121 p := testProvider("aws") 122 p.GetProviderSchemaResponse = getProviderSchemaResponse 123 return p, nil 124 } 125 126 inp := &MockUIInput{ 127 InputReturnMap: map[string]string{ 128 "provider.aws.foo": "bar", 129 "provider.aws.east.foo": "bar", 130 }, 131 } 132 133 ctx := testContext2(t, &ContextOpts{ 134 Providers: map[addrs.Provider]providers.Factory{ 135 addrs.NewDefaultProvider("aws"): func() (providers.Interface, error) { 136 return providerFactory() 137 }, 138 }, 139 UIInput: inp, 140 }) 141 142 var actual []interface{} 143 var lock sync.Mutex 144 145 if diags := ctx.Input(m, InputModeStd); diags.HasErrors() { 146 t.Fatalf("input errors: %s", diags.Err()) 147 } 148 149 plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 150 assertNoErrors(t, diags) 151 152 providerFactory = func() (providers.Interface, error) { 153 p := testProvider("aws") 154 p.GetProviderSchemaResponse = getProviderSchemaResponse 155 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 156 lock.Lock() 157 defer lock.Unlock() 158 actual = append(actual, req.Config.GetAttr("foo").AsString()) 159 return 160 } 161 return p, nil 162 } 163 164 if _, diags := ctx.Apply(plan, m); diags.HasErrors() { 165 t.Fatalf("apply errors: %s", diags.Err()) 166 } 167 168 expected := []interface{}{"bar", "bar"} 169 if !reflect.DeepEqual(actual, expected) { 170 t.Fatalf("wrong result\ngot: %#v\nwant: %#v", actual, expected) 171 } 172 } 173 174 func TestContext2Input_providerOnce(t *testing.T) { 175 m := testModule(t, "input-provider-once") 176 p := testProvider("aws") 177 ctx := testContext2(t, &ContextOpts{ 178 Providers: map[addrs.Provider]providers.Factory{ 179 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 180 }, 181 }) 182 183 if diags := ctx.Input(m, InputModeStd); diags.HasErrors() { 184 t.Fatalf("input errors: %s", diags.Err()) 185 } 186 } 187 188 func TestContext2Input_providerId(t *testing.T) { 189 input := new(MockUIInput) 190 191 m := testModule(t, "input-provider") 192 193 p := testProvider("aws") 194 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 195 Provider: &configschema.Block{ 196 Attributes: map[string]*configschema.Attribute{ 197 "foo": { 198 Type: cty.String, 199 Required: true, 200 Description: "something something", 201 }, 202 }, 203 }, 204 ResourceTypes: map[string]*configschema.Block{ 205 "aws_instance": { 206 Attributes: map[string]*configschema.Attribute{ 207 "id": { 208 Type: cty.String, 209 Computed: true, 210 }, 211 }, 212 }, 213 }, 214 }) 215 216 ctx := testContext2(t, &ContextOpts{ 217 Providers: map[addrs.Provider]providers.Factory{ 218 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 219 }, 220 UIInput: input, 221 }) 222 223 var actual interface{} 224 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 225 actual = req.Config.GetAttr("foo").AsString() 226 return 227 } 228 229 input.InputReturnMap = map[string]string{ 230 "provider.aws.foo": "bar", 231 } 232 233 if diags := ctx.Input(m, InputModeStd); diags.HasErrors() { 234 t.Fatalf("input errors: %s", diags.Err()) 235 } 236 237 plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 238 assertNoErrors(t, diags) 239 240 if _, diags := ctx.Apply(plan, m); diags.HasErrors() { 241 t.Fatalf("apply errors: %s", diags.Err()) 242 } 243 244 if !reflect.DeepEqual(actual, "bar") { 245 t.Fatalf("wrong result\ngot: %#v\nwant: %#v", actual, "bar") 246 } 247 } 248 249 func TestContext2Input_providerOnly(t *testing.T) { 250 input := new(MockUIInput) 251 252 m := testModule(t, "input-provider-vars") 253 p := testProvider("aws") 254 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 255 Provider: &configschema.Block{ 256 Attributes: map[string]*configschema.Attribute{ 257 "foo": { 258 Type: cty.String, 259 Required: true, 260 }, 261 }, 262 }, 263 ResourceTypes: map[string]*configschema.Block{ 264 "aws_instance": { 265 Attributes: map[string]*configschema.Attribute{ 266 "foo": {Type: cty.String, Required: true}, 267 "id": {Type: cty.String, Computed: true}, 268 "type": {Type: cty.String, Computed: true}, 269 }, 270 }, 271 }, 272 }) 273 274 ctx := testContext2(t, &ContextOpts{ 275 Providers: map[addrs.Provider]providers.Factory{ 276 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 277 }, 278 UIInput: input, 279 }) 280 281 input.InputReturnMap = map[string]string{ 282 "provider.aws.foo": "bar", 283 } 284 285 var actual interface{} 286 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 287 actual = req.Config.GetAttr("foo").AsString() 288 return 289 } 290 291 if err := ctx.Input(m, InputModeProvider); err != nil { 292 t.Fatalf("err: %s", err) 293 } 294 295 // NOTE: This is a stale test case from an older version of Terraform 296 // where Input was responsible for prompting for both input variables _and_ 297 // provider configuration arguments, where it was trying to test the case 298 // where we were turning off the mode of prompting for input variables. 299 // That's now always disabled, and so this is essentially the same as the 300 // normal Input test, but we're preserving it until we have time to review 301 // and make sure this isn't inadvertently providing unique test coverage 302 // other than what it set out to test. 303 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 304 Mode: plans.NormalMode, 305 SetVariables: InputValues{ 306 "foo": &InputValue{ 307 Value: cty.StringVal("us-west-2"), 308 SourceType: ValueFromCaller, 309 }, 310 }, 311 }) 312 assertNoErrors(t, diags) 313 314 state, err := ctx.Apply(plan, m) 315 if err != nil { 316 t.Fatalf("err: %s", err) 317 } 318 319 if !reflect.DeepEqual(actual, "bar") { 320 t.Fatalf("wrong result\ngot: %#v\nwant: %#v", actual, "bar") 321 } 322 323 actualStr := strings.TrimSpace(state.String()) 324 expectedStr := strings.TrimSpace(testTofuInputProviderOnlyStr) 325 if actualStr != expectedStr { 326 t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actualStr, expectedStr) 327 } 328 } 329 330 func TestContext2Input_providerVars(t *testing.T) { 331 input := new(MockUIInput) 332 m := testModule(t, "input-provider-with-vars") 333 p := testProvider("aws") 334 ctx := testContext2(t, &ContextOpts{ 335 Providers: map[addrs.Provider]providers.Factory{ 336 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 337 }, 338 UIInput: input, 339 }) 340 341 input.InputReturnMap = map[string]string{ 342 "var.foo": "bar", 343 } 344 345 var actual interface{} 346 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 347 actual = req.Config.GetAttr("foo").AsString() 348 return 349 } 350 if diags := ctx.Input(m, InputModeStd); diags.HasErrors() { 351 t.Fatalf("input errors: %s", diags.Err()) 352 } 353 354 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 355 Mode: plans.NormalMode, 356 SetVariables: InputValues{ 357 "foo": &InputValue{ 358 Value: cty.StringVal("bar"), 359 SourceType: ValueFromCaller, 360 }, 361 }, 362 }) 363 assertNoErrors(t, diags) 364 365 if _, diags := ctx.Apply(plan, m); diags.HasErrors() { 366 t.Fatalf("apply errors: %s", diags.Err()) 367 } 368 369 if !reflect.DeepEqual(actual, "bar") { 370 t.Fatalf("bad: %#v", actual) 371 } 372 } 373 374 func TestContext2Input_providerVarsModuleInherit(t *testing.T) { 375 input := new(MockUIInput) 376 m := testModule(t, "input-provider-with-vars-and-module") 377 p := testProvider("aws") 378 ctx := testContext2(t, &ContextOpts{ 379 Providers: map[addrs.Provider]providers.Factory{ 380 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 381 }, 382 UIInput: input, 383 }) 384 385 if diags := ctx.Input(m, InputModeStd); diags.HasErrors() { 386 t.Fatalf("input errors: %s", diags.Err()) 387 } 388 } 389 390 // adding a list interpolation in fails to interpolate the count variable 391 func TestContext2Input_submoduleTriggersInvalidCount(t *testing.T) { 392 input := new(MockUIInput) 393 m := testModule(t, "input-submodule-count") 394 p := testProvider("aws") 395 ctx := testContext2(t, &ContextOpts{ 396 Providers: map[addrs.Provider]providers.Factory{ 397 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 398 }, 399 UIInput: input, 400 }) 401 402 if diags := ctx.Input(m, InputModeStd); diags.HasErrors() { 403 t.Fatalf("input errors: %s", diags.Err()) 404 } 405 } 406 407 // In this case, a module variable can't be resolved from a data source until 408 // it's refreshed, but it can't be refreshed during Input. 409 func TestContext2Input_dataSourceRequiresRefresh(t *testing.T) { 410 input := new(MockUIInput) 411 p := testProvider("null") 412 m := testModule(t, "input-module-data-vars") 413 414 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 415 DataSources: map[string]*configschema.Block{ 416 "null_data_source": { 417 Attributes: map[string]*configschema.Attribute{ 418 "foo": {Type: cty.List(cty.String), Optional: true}, 419 }, 420 }, 421 }, 422 }) 423 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 424 return providers.ReadDataSourceResponse{ 425 State: req.Config, 426 } 427 } 428 429 state := states.BuildState(func(s *states.SyncState) { 430 s.SetResourceInstanceCurrent( 431 addrs.Resource{ 432 Mode: addrs.DataResourceMode, 433 Type: "null_data_source", 434 Name: "bar", 435 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 436 &states.ResourceInstanceObjectSrc{ 437 AttrsFlat: map[string]string{ 438 "id": "-", 439 "foo.#": "1", 440 "foo.0": "a", 441 // foo.1 exists in the data source, but needs to be refreshed. 442 }, 443 Status: states.ObjectReady, 444 }, 445 addrs.AbsProviderConfig{ 446 Provider: addrs.NewDefaultProvider("null"), 447 Module: addrs.RootModule, 448 }, 449 ) 450 }) 451 452 ctx := testContext2(t, &ContextOpts{ 453 Providers: map[addrs.Provider]providers.Factory{ 454 addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), 455 }, 456 UIInput: input, 457 }) 458 459 if diags := ctx.Input(m, InputModeStd); diags.HasErrors() { 460 t.Fatalf("input errors: %s", diags.Err()) 461 } 462 463 // ensure that plan works after Refresh. This is a legacy test that 464 // doesn't really make sense anymore, because Refresh is really just 465 // a wrapper around plan anyway, but we're keeping it until we get a 466 // chance to review and check whether it's giving us any additional 467 // test coverage aside from what it's specifically intending to test. 468 if _, diags := ctx.Refresh(m, state, DefaultPlanOpts); diags.HasErrors() { 469 t.Fatalf("refresh errors: %s", diags.Err()) 470 } 471 if _, diags := ctx.Plan(m, state, DefaultPlanOpts); diags.HasErrors() { 472 t.Fatalf("plan errors: %s", diags.Err()) 473 } 474 }