github.com/opentofu/opentofu@v1.7.1/internal/addrs/parse_target_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 addrs
     7  
     8  import (
     9  	"testing"
    10  
    11  	"github.com/go-test/deep"
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/hashicorp/hcl/v2/hclsyntax"
    14  	"github.com/opentofu/opentofu/internal/tfdiags"
    15  )
    16  
    17  func TestParseTarget(t *testing.T) {
    18  	tests := []struct {
    19  		Input   string
    20  		Want    *Target
    21  		WantErr string
    22  	}{
    23  		{
    24  			`module.foo`,
    25  			&Target{
    26  				Subject: ModuleInstance{
    27  					{
    28  						Name: "foo",
    29  					},
    30  				},
    31  				SourceRange: tfdiags.SourceRange{
    32  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
    33  					End:   tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10},
    34  				},
    35  			},
    36  			``,
    37  		},
    38  		{
    39  			`module.foo[2]`,
    40  			&Target{
    41  				Subject: ModuleInstance{
    42  					{
    43  						Name:        "foo",
    44  						InstanceKey: IntKey(2),
    45  					},
    46  				},
    47  				SourceRange: tfdiags.SourceRange{
    48  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
    49  					End:   tfdiags.SourcePos{Line: 1, Column: 14, Byte: 13},
    50  				},
    51  			},
    52  			``,
    53  		},
    54  		{
    55  			`module.foo[2].module.bar`,
    56  			&Target{
    57  				Subject: ModuleInstance{
    58  					{
    59  						Name:        "foo",
    60  						InstanceKey: IntKey(2),
    61  					},
    62  					{
    63  						Name: "bar",
    64  					},
    65  				},
    66  				SourceRange: tfdiags.SourceRange{
    67  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
    68  					End:   tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24},
    69  				},
    70  			},
    71  			``,
    72  		},
    73  		{
    74  			`aws_instance.foo`,
    75  			&Target{
    76  				Subject: AbsResource{
    77  					Resource: Resource{
    78  						Mode: ManagedResourceMode,
    79  						Type: "aws_instance",
    80  						Name: "foo",
    81  					},
    82  					Module: RootModuleInstance,
    83  				},
    84  				SourceRange: tfdiags.SourceRange{
    85  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
    86  					End:   tfdiags.SourcePos{Line: 1, Column: 17, Byte: 16},
    87  				},
    88  			},
    89  			``,
    90  		},
    91  		{
    92  			`aws_instance.foo[1]`,
    93  			&Target{
    94  				Subject: AbsResourceInstance{
    95  					Resource: ResourceInstance{
    96  						Resource: Resource{
    97  							Mode: ManagedResourceMode,
    98  							Type: "aws_instance",
    99  							Name: "foo",
   100  						},
   101  						Key: IntKey(1),
   102  					},
   103  					Module: RootModuleInstance,
   104  				},
   105  				SourceRange: tfdiags.SourceRange{
   106  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   107  					End:   tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19},
   108  				},
   109  			},
   110  			``,
   111  		},
   112  		{
   113  			`data.aws_instance.foo`,
   114  			&Target{
   115  				Subject: AbsResource{
   116  					Resource: Resource{
   117  						Mode: DataResourceMode,
   118  						Type: "aws_instance",
   119  						Name: "foo",
   120  					},
   121  					Module: RootModuleInstance,
   122  				},
   123  				SourceRange: tfdiags.SourceRange{
   124  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   125  					End:   tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21},
   126  				},
   127  			},
   128  			``,
   129  		},
   130  		{
   131  			`data.aws_instance.foo[1]`,
   132  			&Target{
   133  				Subject: AbsResourceInstance{
   134  					Resource: ResourceInstance{
   135  						Resource: Resource{
   136  							Mode: DataResourceMode,
   137  							Type: "aws_instance",
   138  							Name: "foo",
   139  						},
   140  						Key: IntKey(1),
   141  					},
   142  					Module: RootModuleInstance,
   143  				},
   144  				SourceRange: tfdiags.SourceRange{
   145  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   146  					End:   tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24},
   147  				},
   148  			},
   149  			``,
   150  		},
   151  		{
   152  			`module.foo.aws_instance.bar`,
   153  			&Target{
   154  				Subject: AbsResource{
   155  					Resource: Resource{
   156  						Mode: ManagedResourceMode,
   157  						Type: "aws_instance",
   158  						Name: "bar",
   159  					},
   160  					Module: ModuleInstance{
   161  						{Name: "foo"},
   162  					},
   163  				},
   164  				SourceRange: tfdiags.SourceRange{
   165  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   166  					End:   tfdiags.SourcePos{Line: 1, Column: 28, Byte: 27},
   167  				},
   168  			},
   169  			``,
   170  		},
   171  		{
   172  			`module.foo.module.bar.aws_instance.baz`,
   173  			&Target{
   174  				Subject: AbsResource{
   175  					Resource: Resource{
   176  						Mode: ManagedResourceMode,
   177  						Type: "aws_instance",
   178  						Name: "baz",
   179  					},
   180  					Module: ModuleInstance{
   181  						{Name: "foo"},
   182  						{Name: "bar"},
   183  					},
   184  				},
   185  				SourceRange: tfdiags.SourceRange{
   186  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   187  					End:   tfdiags.SourcePos{Line: 1, Column: 39, Byte: 38},
   188  				},
   189  			},
   190  			``,
   191  		},
   192  		{
   193  			`module.foo.module.bar.aws_instance.baz["hello"]`,
   194  			&Target{
   195  				Subject: AbsResourceInstance{
   196  					Resource: ResourceInstance{
   197  						Resource: Resource{
   198  							Mode: ManagedResourceMode,
   199  							Type: "aws_instance",
   200  							Name: "baz",
   201  						},
   202  						Key: StringKey("hello"),
   203  					},
   204  					Module: ModuleInstance{
   205  						{Name: "foo"},
   206  						{Name: "bar"},
   207  					},
   208  				},
   209  				SourceRange: tfdiags.SourceRange{
   210  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   211  					End:   tfdiags.SourcePos{Line: 1, Column: 48, Byte: 47},
   212  				},
   213  			},
   214  			``,
   215  		},
   216  		{
   217  			`module.foo.data.aws_instance.bar`,
   218  			&Target{
   219  				Subject: AbsResource{
   220  					Resource: Resource{
   221  						Mode: DataResourceMode,
   222  						Type: "aws_instance",
   223  						Name: "bar",
   224  					},
   225  					Module: ModuleInstance{
   226  						{Name: "foo"},
   227  					},
   228  				},
   229  				SourceRange: tfdiags.SourceRange{
   230  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   231  					End:   tfdiags.SourcePos{Line: 1, Column: 33, Byte: 32},
   232  				},
   233  			},
   234  			``,
   235  		},
   236  		{
   237  			`module.foo.module.bar.data.aws_instance.baz`,
   238  			&Target{
   239  				Subject: AbsResource{
   240  					Resource: Resource{
   241  						Mode: DataResourceMode,
   242  						Type: "aws_instance",
   243  						Name: "baz",
   244  					},
   245  					Module: ModuleInstance{
   246  						{Name: "foo"},
   247  						{Name: "bar"},
   248  					},
   249  				},
   250  				SourceRange: tfdiags.SourceRange{
   251  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   252  					End:   tfdiags.SourcePos{Line: 1, Column: 44, Byte: 43},
   253  				},
   254  			},
   255  			``,
   256  		},
   257  		{
   258  			`module.foo.module.bar[0].data.aws_instance.baz`,
   259  			&Target{
   260  				Subject: AbsResource{
   261  					Resource: Resource{
   262  						Mode: DataResourceMode,
   263  						Type: "aws_instance",
   264  						Name: "baz",
   265  					},
   266  					Module: ModuleInstance{
   267  						{Name: "foo", InstanceKey: NoKey},
   268  						{Name: "bar", InstanceKey: IntKey(0)},
   269  					},
   270  				},
   271  				SourceRange: tfdiags.SourceRange{
   272  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   273  					End:   tfdiags.SourcePos{Line: 1, Column: 47, Byte: 46},
   274  				},
   275  			},
   276  			``,
   277  		},
   278  		{
   279  			`module.foo.module.bar["a"].data.aws_instance.baz["hello"]`,
   280  			&Target{
   281  				Subject: AbsResourceInstance{
   282  					Resource: ResourceInstance{
   283  						Resource: Resource{
   284  							Mode: DataResourceMode,
   285  							Type: "aws_instance",
   286  							Name: "baz",
   287  						},
   288  						Key: StringKey("hello"),
   289  					},
   290  					Module: ModuleInstance{
   291  						{Name: "foo", InstanceKey: NoKey},
   292  						{Name: "bar", InstanceKey: StringKey("a")},
   293  					},
   294  				},
   295  				SourceRange: tfdiags.SourceRange{
   296  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   297  					End:   tfdiags.SourcePos{Line: 1, Column: 58, Byte: 57},
   298  				},
   299  			},
   300  			``,
   301  		},
   302  		{
   303  			`module.foo.module.bar.data.aws_instance.baz["hello"]`,
   304  			&Target{
   305  				Subject: AbsResourceInstance{
   306  					Resource: ResourceInstance{
   307  						Resource: Resource{
   308  							Mode: DataResourceMode,
   309  							Type: "aws_instance",
   310  							Name: "baz",
   311  						},
   312  						Key: StringKey("hello"),
   313  					},
   314  					Module: ModuleInstance{
   315  						{Name: "foo"},
   316  						{Name: "bar"},
   317  					},
   318  				},
   319  				SourceRange: tfdiags.SourceRange{
   320  					Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
   321  					End:   tfdiags.SourcePos{Line: 1, Column: 53, Byte: 52},
   322  				},
   323  			},
   324  			``,
   325  		},
   326  
   327  		{
   328  			`aws_instance`,
   329  			nil,
   330  			`Resource specification must include a resource type and name.`,
   331  		},
   332  		{
   333  			`module`,
   334  			nil,
   335  			`Prefix "module." must be followed by a module name.`,
   336  		},
   337  		{
   338  			`module["baz"]`,
   339  			nil,
   340  			`Prefix "module." must be followed by a module name.`,
   341  		},
   342  		{
   343  			`module.baz.bar`,
   344  			nil,
   345  			`Resource specification must include a resource type and name.`,
   346  		},
   347  		{
   348  			`aws_instance.foo.bar`,
   349  			nil,
   350  			`Resource instance key must be given in square brackets.`,
   351  		},
   352  		{
   353  			`aws_instance.foo[1].baz`,
   354  			nil,
   355  			`Unexpected extra operators after address.`,
   356  		},
   357  	}
   358  
   359  	for _, test := range tests {
   360  		t.Run(test.Input, func(t *testing.T) {
   361  			traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1})
   362  			if travDiags.HasErrors() {
   363  				t.Fatal(travDiags.Error())
   364  			}
   365  
   366  			got, diags := ParseTarget(traversal)
   367  
   368  			switch len(diags) {
   369  			case 0:
   370  				if test.WantErr != "" {
   371  					t.Fatalf("succeeded; want error: %s", test.WantErr)
   372  				}
   373  			case 1:
   374  				if test.WantErr == "" {
   375  					t.Fatalf("unexpected diagnostics: %s", diags.Err())
   376  				}
   377  				if got, want := diags[0].Description().Detail, test.WantErr; got != want {
   378  					t.Fatalf("wrong error\ngot:  %s\nwant: %s", got, want)
   379  				}
   380  			default:
   381  				t.Fatalf("too many diagnostics: %s", diags.Err())
   382  			}
   383  
   384  			if diags.HasErrors() {
   385  				return
   386  			}
   387  
   388  			for _, problem := range deep.Equal(got, test.Want) {
   389  				t.Errorf(problem)
   390  			}
   391  		})
   392  	}
   393  }