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  }