github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/plans/objchange/compatible_test.go (about)

     1  package objchange
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/apparentlymart/go-dump/dump"
     8  	"github.com/zclconf/go-cty/cty"
     9  
    10  	"github.com/eliastor/durgaform/internal/configs/configschema"
    11  	"github.com/eliastor/durgaform/internal/lang/marks"
    12  	"github.com/eliastor/durgaform/internal/tfdiags"
    13  )
    14  
    15  func TestAssertObjectCompatible(t *testing.T) {
    16  	schemaWithFoo := configschema.Block{
    17  		Attributes: map[string]*configschema.Attribute{
    18  			"foo": {Type: cty.String, Optional: true},
    19  		},
    20  	}
    21  	fooBlockValue := cty.ObjectVal(map[string]cty.Value{
    22  		"foo": cty.StringVal("bar"),
    23  	})
    24  	schemaWithFooBar := configschema.Block{
    25  		Attributes: map[string]*configschema.Attribute{
    26  			"foo": {Type: cty.String, Optional: true},
    27  			"bar": {Type: cty.String, Optional: true},
    28  		},
    29  	}
    30  	fooBarBlockValue := cty.ObjectVal(map[string]cty.Value{
    31  		"foo": cty.StringVal("bar"),
    32  		"bar": cty.NullVal(cty.String), // simulating the situation where bar isn't set in the config at all
    33  	})
    34  
    35  	tests := []struct {
    36  		Schema   *configschema.Block
    37  		Planned  cty.Value
    38  		Actual   cty.Value
    39  		WantErrs []string
    40  	}{
    41  		{
    42  			&configschema.Block{},
    43  			cty.EmptyObjectVal,
    44  			cty.EmptyObjectVal,
    45  			nil,
    46  		},
    47  		{
    48  			&configschema.Block{
    49  				Attributes: map[string]*configschema.Attribute{
    50  					"id": {
    51  						Type:     cty.String,
    52  						Computed: true,
    53  					},
    54  					"name": {
    55  						Type:     cty.String,
    56  						Required: true,
    57  					},
    58  				},
    59  			},
    60  			cty.ObjectVal(map[string]cty.Value{
    61  				"id":   cty.UnknownVal(cty.String),
    62  				"name": cty.StringVal("thingy"),
    63  			}),
    64  			cty.ObjectVal(map[string]cty.Value{
    65  				"id":   cty.UnknownVal(cty.String),
    66  				"name": cty.StringVal("thingy"),
    67  			}),
    68  			nil,
    69  		},
    70  		{
    71  			&configschema.Block{
    72  				Attributes: map[string]*configschema.Attribute{
    73  					"id": {
    74  						Type:     cty.String,
    75  						Computed: true,
    76  					},
    77  					"name": {
    78  						Type:     cty.String,
    79  						Required: true,
    80  					},
    81  				},
    82  			},
    83  			cty.ObjectVal(map[string]cty.Value{
    84  				"id":   cty.UnknownVal(cty.String),
    85  				"name": cty.UnknownVal(cty.String),
    86  			}),
    87  			cty.ObjectVal(map[string]cty.Value{
    88  				"id":   cty.UnknownVal(cty.String),
    89  				"name": cty.StringVal("thingy"),
    90  			}),
    91  			nil,
    92  		},
    93  		{
    94  			&configschema.Block{
    95  				Attributes: map[string]*configschema.Attribute{
    96  					"id": {
    97  						Type:     cty.String,
    98  						Computed: true,
    99  					},
   100  					"name": {
   101  						Type:     cty.String,
   102  						Required: true,
   103  					},
   104  				},
   105  			},
   106  			cty.ObjectVal(map[string]cty.Value{
   107  				"id":   cty.UnknownVal(cty.String),
   108  				"name": cty.StringVal("wotsit"),
   109  			}),
   110  			cty.ObjectVal(map[string]cty.Value{
   111  				"id":   cty.UnknownVal(cty.String),
   112  				"name": cty.StringVal("thingy"),
   113  			}),
   114  			[]string{
   115  				`.name: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
   116  			},
   117  		},
   118  		{
   119  			&configschema.Block{
   120  				Attributes: map[string]*configschema.Attribute{
   121  					"id": {
   122  						Type:     cty.String,
   123  						Computed: true,
   124  					},
   125  					"name": {
   126  						Type:      cty.String,
   127  						Required:  true,
   128  						Sensitive: true,
   129  					},
   130  				},
   131  			},
   132  			cty.ObjectVal(map[string]cty.Value{
   133  				"id":   cty.UnknownVal(cty.String),
   134  				"name": cty.StringVal("wotsit"),
   135  			}),
   136  			cty.ObjectVal(map[string]cty.Value{
   137  				"id":   cty.UnknownVal(cty.String),
   138  				"name": cty.StringVal("thingy"),
   139  			}),
   140  			[]string{
   141  				`.name: inconsistent values for sensitive attribute`,
   142  			},
   143  		},
   144  		{
   145  			&configschema.Block{
   146  				Attributes: map[string]*configschema.Attribute{
   147  					"id": {
   148  						Type:     cty.String,
   149  						Computed: true,
   150  					},
   151  					"name": {
   152  						Type:     cty.String,
   153  						Required: true,
   154  					},
   155  				},
   156  			},
   157  			cty.ObjectVal(map[string]cty.Value{
   158  				"id":   cty.UnknownVal(cty.String),
   159  				"name": cty.StringVal("wotsit").Mark(marks.Sensitive),
   160  			}),
   161  			cty.ObjectVal(map[string]cty.Value{
   162  				"id":   cty.UnknownVal(cty.String),
   163  				"name": cty.StringVal("thingy"),
   164  			}),
   165  			[]string{
   166  				`.name: inconsistent values for sensitive attribute`,
   167  			},
   168  		},
   169  		{
   170  			&configschema.Block{
   171  				Attributes: map[string]*configschema.Attribute{
   172  					"id": {
   173  						Type:     cty.String,
   174  						Computed: true,
   175  					},
   176  					"name": {
   177  						Type:     cty.String,
   178  						Required: true,
   179  					},
   180  				},
   181  			},
   182  			cty.ObjectVal(map[string]cty.Value{
   183  				"id":   cty.UnknownVal(cty.String),
   184  				"name": cty.StringVal("wotsit"),
   185  			}),
   186  			cty.ObjectVal(map[string]cty.Value{
   187  				"id":   cty.UnknownVal(cty.String),
   188  				"name": cty.StringVal("thingy").Mark(marks.Sensitive),
   189  			}),
   190  			[]string{
   191  				`.name: inconsistent values for sensitive attribute`,
   192  			},
   193  		},
   194  		{
   195  			// This tests the codepath that leads to couldHaveUnknownBlockPlaceholder,
   196  			// where a set may be sensitive and need to be unmarked before it
   197  			// is iterated upon
   198  			&configschema.Block{
   199  				BlockTypes: map[string]*configschema.NestedBlock{
   200  					"configuration": {
   201  						Nesting: configschema.NestingList,
   202  						Block: configschema.Block{
   203  							BlockTypes: map[string]*configschema.NestedBlock{
   204  								"sensitive_fields": {
   205  									Nesting: configschema.NestingSet,
   206  									Block:   schemaWithFoo,
   207  								},
   208  							},
   209  						},
   210  					},
   211  				},
   212  			},
   213  			cty.ObjectVal(map[string]cty.Value{
   214  				"configuration": cty.TupleVal([]cty.Value{
   215  					cty.ObjectVal(map[string]cty.Value{
   216  						"sensitive_fields": cty.SetVal([]cty.Value{
   217  							cty.ObjectVal(map[string]cty.Value{
   218  								"foo": cty.StringVal("secret"),
   219  							}),
   220  						}).Mark(marks.Sensitive),
   221  					}),
   222  				}),
   223  			}),
   224  			cty.ObjectVal(map[string]cty.Value{
   225  				"configuration": cty.TupleVal([]cty.Value{
   226  					cty.ObjectVal(map[string]cty.Value{
   227  						"sensitive_fields": cty.SetVal([]cty.Value{
   228  							cty.ObjectVal(map[string]cty.Value{
   229  								"foo": cty.StringVal("secret"),
   230  							}),
   231  						}).Mark(marks.Sensitive),
   232  					}),
   233  				}),
   234  			}),
   235  			nil,
   236  		},
   237  		{
   238  			&configschema.Block{
   239  				Attributes: map[string]*configschema.Attribute{
   240  					"id": {
   241  						Type:     cty.String,
   242  						Computed: true,
   243  					},
   244  					"stuff": {
   245  						Type:     cty.DynamicPseudoType,
   246  						Required: true,
   247  					},
   248  				},
   249  			},
   250  			cty.ObjectVal(map[string]cty.Value{
   251  				"id":    cty.UnknownVal(cty.String),
   252  				"stuff": cty.DynamicVal,
   253  			}),
   254  			cty.ObjectVal(map[string]cty.Value{
   255  				"id":    cty.UnknownVal(cty.String),
   256  				"stuff": cty.StringVal("thingy"),
   257  			}),
   258  			[]string{},
   259  		},
   260  		{
   261  			&configschema.Block{
   262  				Attributes: map[string]*configschema.Attribute{
   263  					"obj": {
   264  						Type: cty.Object(map[string]cty.Type{
   265  							"stuff": cty.DynamicPseudoType,
   266  						}),
   267  					},
   268  				},
   269  			},
   270  			cty.ObjectVal(map[string]cty.Value{
   271  				"obj": cty.ObjectVal(map[string]cty.Value{
   272  					"stuff": cty.DynamicVal,
   273  				}),
   274  			}),
   275  			cty.ObjectVal(map[string]cty.Value{
   276  				"obj": cty.ObjectVal(map[string]cty.Value{
   277  					"stuff": cty.NumberIntVal(3),
   278  				}),
   279  			}),
   280  			[]string{},
   281  		},
   282  		{
   283  			&configschema.Block{
   284  				Attributes: map[string]*configschema.Attribute{
   285  					"id": {
   286  						Type:     cty.String,
   287  						Computed: true,
   288  					},
   289  					"stuff": {
   290  						Type:     cty.DynamicPseudoType,
   291  						Required: true,
   292  					},
   293  				},
   294  			},
   295  			cty.ObjectVal(map[string]cty.Value{
   296  				"id":    cty.UnknownVal(cty.String),
   297  				"stuff": cty.StringVal("wotsit"),
   298  			}),
   299  			cty.ObjectVal(map[string]cty.Value{
   300  				"id":    cty.UnknownVal(cty.String),
   301  				"stuff": cty.StringVal("thingy"),
   302  			}),
   303  			[]string{
   304  				`.stuff: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
   305  			},
   306  		},
   307  		{
   308  			&configschema.Block{
   309  				Attributes: map[string]*configschema.Attribute{
   310  					"id": {
   311  						Type:     cty.String,
   312  						Computed: true,
   313  					},
   314  					"stuff": {
   315  						Type:     cty.DynamicPseudoType,
   316  						Required: true,
   317  					},
   318  				},
   319  			},
   320  			cty.ObjectVal(map[string]cty.Value{
   321  				"id":    cty.UnknownVal(cty.String),
   322  				"stuff": cty.StringVal("true"),
   323  			}),
   324  			cty.ObjectVal(map[string]cty.Value{
   325  				"id":    cty.UnknownVal(cty.String),
   326  				"stuff": cty.True,
   327  			}),
   328  			[]string{
   329  				`.stuff: wrong final value type: string required`,
   330  			},
   331  		},
   332  		{
   333  			&configschema.Block{
   334  				Attributes: map[string]*configschema.Attribute{
   335  					"id": {
   336  						Type:     cty.String,
   337  						Computed: true,
   338  					},
   339  					"stuff": {
   340  						Type:     cty.DynamicPseudoType,
   341  						Required: true,
   342  					},
   343  				},
   344  			},
   345  			cty.ObjectVal(map[string]cty.Value{
   346  				"id":    cty.UnknownVal(cty.String),
   347  				"stuff": cty.DynamicVal,
   348  			}),
   349  			cty.ObjectVal(map[string]cty.Value{
   350  				"id":    cty.UnknownVal(cty.String),
   351  				"stuff": cty.EmptyObjectVal,
   352  			}),
   353  			nil,
   354  		},
   355  		{
   356  			&configschema.Block{
   357  				Attributes: map[string]*configschema.Attribute{
   358  					"id": {
   359  						Type:     cty.String,
   360  						Computed: true,
   361  					},
   362  					"stuff": {
   363  						Type:     cty.DynamicPseudoType,
   364  						Required: true,
   365  					},
   366  				},
   367  			},
   368  			cty.ObjectVal(map[string]cty.Value{
   369  				"id": cty.UnknownVal(cty.String),
   370  				"stuff": cty.ObjectVal(map[string]cty.Value{
   371  					"nonsense": cty.StringVal("yup"),
   372  				}),
   373  			}),
   374  			cty.ObjectVal(map[string]cty.Value{
   375  				"id":    cty.UnknownVal(cty.String),
   376  				"stuff": cty.EmptyObjectVal,
   377  			}),
   378  			[]string{
   379  				`.stuff: wrong final value type: attribute "nonsense" is required`,
   380  			},
   381  		},
   382  		{
   383  			&configschema.Block{
   384  				Attributes: map[string]*configschema.Attribute{
   385  					"id": {
   386  						Type:     cty.String,
   387  						Computed: true,
   388  					},
   389  					"tags": {
   390  						Type:     cty.Map(cty.String),
   391  						Optional: true,
   392  					},
   393  				},
   394  			},
   395  			cty.ObjectVal(map[string]cty.Value{
   396  				"id": cty.UnknownVal(cty.String),
   397  				"tags": cty.MapVal(map[string]cty.Value{
   398  					"Name": cty.StringVal("thingy"),
   399  				}),
   400  			}),
   401  			cty.ObjectVal(map[string]cty.Value{
   402  				"id": cty.UnknownVal(cty.String),
   403  				"tags": cty.MapVal(map[string]cty.Value{
   404  					"Name": cty.StringVal("thingy"),
   405  				}),
   406  			}),
   407  			nil,
   408  		},
   409  		{
   410  			&configschema.Block{
   411  				Attributes: map[string]*configschema.Attribute{
   412  					"id": {
   413  						Type:     cty.String,
   414  						Computed: true,
   415  					},
   416  					"tags": {
   417  						Type:     cty.Map(cty.String),
   418  						Optional: true,
   419  					},
   420  				},
   421  			},
   422  			cty.ObjectVal(map[string]cty.Value{
   423  				"id": cty.UnknownVal(cty.String),
   424  				"tags": cty.MapVal(map[string]cty.Value{
   425  					"Name": cty.UnknownVal(cty.String),
   426  				}),
   427  			}),
   428  			cty.ObjectVal(map[string]cty.Value{
   429  				"id": cty.UnknownVal(cty.String),
   430  				"tags": cty.MapVal(map[string]cty.Value{
   431  					"Name": cty.StringVal("thingy"),
   432  				}),
   433  			}),
   434  			nil,
   435  		},
   436  		{
   437  			&configschema.Block{
   438  				Attributes: map[string]*configschema.Attribute{
   439  					"id": {
   440  						Type:     cty.String,
   441  						Computed: true,
   442  					},
   443  					"tags": {
   444  						Type:     cty.Map(cty.String),
   445  						Optional: true,
   446  					},
   447  				},
   448  			},
   449  			cty.ObjectVal(map[string]cty.Value{
   450  				"id": cty.UnknownVal(cty.String),
   451  				"tags": cty.MapVal(map[string]cty.Value{
   452  					"Name": cty.StringVal("wotsit"),
   453  				}),
   454  			}),
   455  			cty.ObjectVal(map[string]cty.Value{
   456  				"id": cty.UnknownVal(cty.String),
   457  				"tags": cty.MapVal(map[string]cty.Value{
   458  					"Name": cty.StringVal("thingy"),
   459  				}),
   460  			}),
   461  			[]string{
   462  				`.tags["Name"]: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
   463  			},
   464  		},
   465  		{
   466  			&configschema.Block{
   467  				Attributes: map[string]*configschema.Attribute{
   468  					"id": {
   469  						Type:     cty.String,
   470  						Computed: true,
   471  					},
   472  					"tags": {
   473  						Type:     cty.Map(cty.String),
   474  						Optional: true,
   475  					},
   476  				},
   477  			},
   478  			cty.ObjectVal(map[string]cty.Value{
   479  				"id": cty.UnknownVal(cty.String),
   480  				"tags": cty.MapVal(map[string]cty.Value{
   481  					"Name": cty.StringVal("thingy"),
   482  				}),
   483  			}),
   484  			cty.ObjectVal(map[string]cty.Value{
   485  				"id": cty.UnknownVal(cty.String),
   486  				"tags": cty.MapVal(map[string]cty.Value{
   487  					"Name": cty.StringVal("thingy"),
   488  					"Env":  cty.StringVal("production"),
   489  				}),
   490  			}),
   491  			[]string{
   492  				`.tags: new element "Env" has appeared`,
   493  			},
   494  		},
   495  		{
   496  			&configschema.Block{
   497  				Attributes: map[string]*configschema.Attribute{
   498  					"id": {
   499  						Type:     cty.String,
   500  						Computed: true,
   501  					},
   502  					"tags": {
   503  						Type:     cty.Map(cty.String),
   504  						Optional: true,
   505  					},
   506  				},
   507  			},
   508  			cty.ObjectVal(map[string]cty.Value{
   509  				"id": cty.UnknownVal(cty.String),
   510  				"tags": cty.MapVal(map[string]cty.Value{
   511  					"Name": cty.StringVal("thingy"),
   512  				}),
   513  			}),
   514  			cty.ObjectVal(map[string]cty.Value{
   515  				"id":   cty.UnknownVal(cty.String),
   516  				"tags": cty.MapValEmpty(cty.String),
   517  			}),
   518  			[]string{
   519  				`.tags: element "Name" has vanished`,
   520  			},
   521  		},
   522  		{
   523  			&configschema.Block{
   524  				Attributes: map[string]*configschema.Attribute{
   525  					"id": {
   526  						Type:     cty.String,
   527  						Computed: true,
   528  					},
   529  					"tags": {
   530  						Type:     cty.Map(cty.String),
   531  						Optional: true,
   532  					},
   533  				},
   534  			},
   535  			cty.ObjectVal(map[string]cty.Value{
   536  				"id": cty.UnknownVal(cty.String),
   537  				"tags": cty.MapVal(map[string]cty.Value{
   538  					"Name": cty.UnknownVal(cty.String),
   539  				}),
   540  			}),
   541  			cty.ObjectVal(map[string]cty.Value{
   542  				"id": cty.UnknownVal(cty.String),
   543  				"tags": cty.MapVal(map[string]cty.Value{
   544  					"Name": cty.NullVal(cty.String),
   545  				}),
   546  			}),
   547  			nil,
   548  		},
   549  		{
   550  			&configschema.Block{
   551  				Attributes: map[string]*configschema.Attribute{
   552  					"id": {
   553  						Type:     cty.String,
   554  						Computed: true,
   555  					},
   556  					"zones": {
   557  						Type:     cty.Set(cty.String),
   558  						Optional: true,
   559  					},
   560  				},
   561  			},
   562  			cty.ObjectVal(map[string]cty.Value{
   563  				"id": cty.UnknownVal(cty.String),
   564  				"zones": cty.SetVal([]cty.Value{
   565  					cty.StringVal("thingy"),
   566  				}),
   567  			}),
   568  			cty.ObjectVal(map[string]cty.Value{
   569  				"id": cty.UnknownVal(cty.String),
   570  				"zones": cty.SetVal([]cty.Value{
   571  					cty.StringVal("thingy"),
   572  				}),
   573  			}),
   574  			nil,
   575  		},
   576  		{
   577  			&configschema.Block{
   578  				Attributes: map[string]*configschema.Attribute{
   579  					"id": {
   580  						Type:     cty.String,
   581  						Computed: true,
   582  					},
   583  					"zones": {
   584  						Type:     cty.Set(cty.String),
   585  						Optional: true,
   586  					},
   587  				},
   588  			},
   589  			cty.ObjectVal(map[string]cty.Value{
   590  				"id": cty.UnknownVal(cty.String),
   591  				"zones": cty.SetVal([]cty.Value{
   592  					cty.StringVal("thingy"),
   593  				}),
   594  			}),
   595  			cty.ObjectVal(map[string]cty.Value{
   596  				"id": cty.UnknownVal(cty.String),
   597  				"zones": cty.SetVal([]cty.Value{
   598  					cty.StringVal("thingy"),
   599  					cty.StringVal("wotsit"),
   600  				}),
   601  			}),
   602  			[]string{
   603  				`.zones: actual set element cty.StringVal("wotsit") does not correlate with any element in plan`,
   604  				`.zones: length changed from 1 to 2`,
   605  			},
   606  		},
   607  		{
   608  			&configschema.Block{
   609  				Attributes: map[string]*configschema.Attribute{
   610  					"id": {
   611  						Type:     cty.String,
   612  						Computed: true,
   613  					},
   614  					"zones": {
   615  						Type:     cty.Set(cty.String),
   616  						Optional: true,
   617  					},
   618  				},
   619  			},
   620  			cty.ObjectVal(map[string]cty.Value{
   621  				"id": cty.UnknownVal(cty.String),
   622  				"zones": cty.SetVal([]cty.Value{
   623  					cty.UnknownVal(cty.String),
   624  					cty.UnknownVal(cty.String),
   625  				}),
   626  			}),
   627  			cty.ObjectVal(map[string]cty.Value{
   628  				"id": cty.UnknownVal(cty.String),
   629  				"zones": cty.SetVal([]cty.Value{
   630  					// Imagine that both of our unknown values ultimately resolved to "thingy",
   631  					// causing them to collapse into a single element. That's valid,
   632  					// even though it's also a little confusing and counter-intuitive.
   633  					cty.StringVal("thingy"),
   634  				}),
   635  			}),
   636  			nil,
   637  		},
   638  		{
   639  			&configschema.Block{
   640  				Attributes: map[string]*configschema.Attribute{
   641  					"id": {
   642  						Type:     cty.String,
   643  						Computed: true,
   644  					},
   645  					"names": {
   646  						Type:     cty.List(cty.String),
   647  						Optional: true,
   648  					},
   649  				},
   650  			},
   651  			cty.ObjectVal(map[string]cty.Value{
   652  				"id": cty.UnknownVal(cty.String),
   653  				"names": cty.ListVal([]cty.Value{
   654  					cty.StringVal("thingy"),
   655  				}),
   656  			}),
   657  			cty.ObjectVal(map[string]cty.Value{
   658  				"id": cty.UnknownVal(cty.String),
   659  				"names": cty.ListVal([]cty.Value{
   660  					cty.StringVal("thingy"),
   661  				}),
   662  			}),
   663  			nil,
   664  		},
   665  		{
   666  			&configschema.Block{
   667  				Attributes: map[string]*configschema.Attribute{
   668  					"id": {
   669  						Type:     cty.String,
   670  						Computed: true,
   671  					},
   672  					"names": {
   673  						Type:     cty.List(cty.String),
   674  						Optional: true,
   675  					},
   676  				},
   677  			},
   678  			cty.ObjectVal(map[string]cty.Value{
   679  				"id":    cty.UnknownVal(cty.String),
   680  				"names": cty.UnknownVal(cty.List(cty.String)),
   681  			}),
   682  			cty.ObjectVal(map[string]cty.Value{
   683  				"id": cty.UnknownVal(cty.String),
   684  				"names": cty.ListVal([]cty.Value{
   685  					cty.StringVal("thingy"),
   686  				}),
   687  			}),
   688  			nil,
   689  		},
   690  		{
   691  			&configschema.Block{
   692  				Attributes: map[string]*configschema.Attribute{
   693  					"id": {
   694  						Type:     cty.String,
   695  						Computed: true,
   696  					},
   697  					"names": {
   698  						Type:     cty.List(cty.String),
   699  						Optional: true,
   700  					},
   701  				},
   702  			},
   703  			cty.ObjectVal(map[string]cty.Value{
   704  				"id": cty.UnknownVal(cty.String),
   705  				"names": cty.ListVal([]cty.Value{
   706  					cty.UnknownVal(cty.String),
   707  				}),
   708  			}),
   709  			cty.ObjectVal(map[string]cty.Value{
   710  				"id": cty.UnknownVal(cty.String),
   711  				"names": cty.ListVal([]cty.Value{
   712  					cty.StringVal("thingy"),
   713  				}),
   714  			}),
   715  			nil,
   716  		},
   717  		{
   718  			&configschema.Block{
   719  				Attributes: map[string]*configschema.Attribute{
   720  					"id": {
   721  						Type:     cty.String,
   722  						Computed: true,
   723  					},
   724  					"names": {
   725  						Type:     cty.List(cty.String),
   726  						Optional: true,
   727  					},
   728  				},
   729  			},
   730  			cty.ObjectVal(map[string]cty.Value{
   731  				"id": cty.UnknownVal(cty.String),
   732  				"names": cty.ListVal([]cty.Value{
   733  					cty.StringVal("thingy"),
   734  					cty.UnknownVal(cty.String),
   735  				}),
   736  			}),
   737  			cty.ObjectVal(map[string]cty.Value{
   738  				"id": cty.UnknownVal(cty.String),
   739  				"names": cty.ListVal([]cty.Value{
   740  					cty.StringVal("thingy"),
   741  					cty.StringVal("wotsit"),
   742  				}),
   743  			}),
   744  			nil,
   745  		},
   746  		{
   747  			&configschema.Block{
   748  				Attributes: map[string]*configschema.Attribute{
   749  					"id": {
   750  						Type:     cty.String,
   751  						Computed: true,
   752  					},
   753  					"names": {
   754  						Type:     cty.List(cty.String),
   755  						Optional: true,
   756  					},
   757  				},
   758  			},
   759  			cty.ObjectVal(map[string]cty.Value{
   760  				"id": cty.UnknownVal(cty.String),
   761  				"names": cty.ListVal([]cty.Value{
   762  					cty.UnknownVal(cty.String),
   763  					cty.StringVal("thingy"),
   764  				}),
   765  			}),
   766  			cty.ObjectVal(map[string]cty.Value{
   767  				"id": cty.UnknownVal(cty.String),
   768  				"names": cty.ListVal([]cty.Value{
   769  					cty.StringVal("thingy"),
   770  					cty.StringVal("wotsit"),
   771  				}),
   772  			}),
   773  			[]string{
   774  				`.names[1]: was cty.StringVal("thingy"), but now cty.StringVal("wotsit")`,
   775  			},
   776  		},
   777  		{
   778  			&configschema.Block{
   779  				Attributes: map[string]*configschema.Attribute{
   780  					"id": {
   781  						Type:     cty.String,
   782  						Computed: true,
   783  					},
   784  					"names": {
   785  						Type:     cty.List(cty.String),
   786  						Optional: true,
   787  					},
   788  				},
   789  			},
   790  			cty.ObjectVal(map[string]cty.Value{
   791  				"id": cty.UnknownVal(cty.String),
   792  				"names": cty.ListVal([]cty.Value{
   793  					cty.UnknownVal(cty.String),
   794  				}),
   795  			}),
   796  			cty.ObjectVal(map[string]cty.Value{
   797  				"id": cty.UnknownVal(cty.String),
   798  				"names": cty.ListVal([]cty.Value{
   799  					cty.StringVal("thingy"),
   800  					cty.StringVal("wotsit"),
   801  				}),
   802  			}),
   803  			[]string{
   804  				`.names: new element 1 has appeared`,
   805  			},
   806  		},
   807  
   808  		// NestingSingle blocks
   809  		{
   810  			&configschema.Block{
   811  				BlockTypes: map[string]*configschema.NestedBlock{
   812  					"key": {
   813  						Nesting: configschema.NestingSingle,
   814  						Block:   configschema.Block{},
   815  					},
   816  				},
   817  			},
   818  			cty.ObjectVal(map[string]cty.Value{
   819  				"key": cty.EmptyObjectVal,
   820  			}),
   821  			cty.ObjectVal(map[string]cty.Value{
   822  				"key": cty.EmptyObjectVal,
   823  			}),
   824  			nil,
   825  		},
   826  		{
   827  			&configschema.Block{
   828  				BlockTypes: map[string]*configschema.NestedBlock{
   829  					"key": {
   830  						Nesting: configschema.NestingSingle,
   831  						Block:   configschema.Block{},
   832  					},
   833  				},
   834  			},
   835  			cty.ObjectVal(map[string]cty.Value{
   836  				"key": cty.UnknownVal(cty.EmptyObject),
   837  			}),
   838  			cty.ObjectVal(map[string]cty.Value{
   839  				"key": cty.EmptyObjectVal,
   840  			}),
   841  			nil,
   842  		},
   843  		{
   844  			&configschema.Block{
   845  				BlockTypes: map[string]*configschema.NestedBlock{
   846  					"key": {
   847  						Nesting: configschema.NestingSingle,
   848  						Block: configschema.Block{
   849  							Attributes: map[string]*configschema.Attribute{
   850  								"foo": {
   851  									Type:     cty.String,
   852  									Optional: true,
   853  								},
   854  							},
   855  						},
   856  					},
   857  				},
   858  			},
   859  			cty.ObjectVal(map[string]cty.Value{
   860  				"key": cty.NullVal(cty.Object(map[string]cty.Type{
   861  					"foo": cty.String,
   862  				})),
   863  			}),
   864  			cty.ObjectVal(map[string]cty.Value{
   865  				"key": cty.ObjectVal(map[string]cty.Value{
   866  					"foo": cty.StringVal("hello"),
   867  				}),
   868  			}),
   869  			[]string{
   870  				`.key: was absent, but now present`,
   871  			},
   872  		},
   873  		{
   874  			&configschema.Block{
   875  				BlockTypes: map[string]*configschema.NestedBlock{
   876  					"key": {
   877  						Nesting: configschema.NestingSingle,
   878  						Block: configschema.Block{
   879  							Attributes: map[string]*configschema.Attribute{
   880  								"foo": {
   881  									Type:     cty.String,
   882  									Optional: true,
   883  								},
   884  							},
   885  						},
   886  					},
   887  				},
   888  			},
   889  			cty.ObjectVal(map[string]cty.Value{
   890  				"key": cty.ObjectVal(map[string]cty.Value{
   891  					"foo": cty.StringVal("hello"),
   892  				}),
   893  			}),
   894  			cty.ObjectVal(map[string]cty.Value{
   895  				"key": cty.NullVal(cty.Object(map[string]cty.Type{
   896  					"foo": cty.String,
   897  				})),
   898  			}),
   899  			[]string{
   900  				`.key: was present, but now absent`,
   901  			},
   902  		},
   903  		{
   904  			&configschema.Block{
   905  				BlockTypes: map[string]*configschema.NestedBlock{
   906  					"key": {
   907  						Nesting: configschema.NestingSingle,
   908  						Block: configschema.Block{
   909  							Attributes: map[string]*configschema.Attribute{
   910  								"foo": {
   911  									Type:     cty.String,
   912  									Optional: true,
   913  								},
   914  							},
   915  						},
   916  					},
   917  				},
   918  			},
   919  			cty.UnknownVal(cty.Object(map[string]cty.Type{
   920  				"key": cty.Object(map[string]cty.Type{
   921  					"foo": cty.String,
   922  				}),
   923  			})),
   924  			cty.ObjectVal(map[string]cty.Value{
   925  				"key": cty.NullVal(cty.Object(map[string]cty.Type{
   926  					"foo": cty.String,
   927  				})),
   928  			}),
   929  			nil,
   930  		},
   931  
   932  		// NestingList blocks
   933  		{
   934  			&configschema.Block{
   935  				BlockTypes: map[string]*configschema.NestedBlock{
   936  					"key": {
   937  						Nesting: configschema.NestingList,
   938  						Block:   schemaWithFoo,
   939  					},
   940  				},
   941  			},
   942  			cty.ObjectVal(map[string]cty.Value{
   943  				"key": cty.ListVal([]cty.Value{
   944  					fooBlockValue,
   945  				}),
   946  			}),
   947  			cty.ObjectVal(map[string]cty.Value{
   948  				"key": cty.ListVal([]cty.Value{
   949  					fooBlockValue,
   950  				}),
   951  			}),
   952  			nil,
   953  		},
   954  		{
   955  			&configschema.Block{
   956  				BlockTypes: map[string]*configschema.NestedBlock{
   957  					"key": {
   958  						Nesting: configschema.NestingList,
   959  						Block:   schemaWithFoo,
   960  					},
   961  				},
   962  			},
   963  			cty.ObjectVal(map[string]cty.Value{
   964  				"key": cty.TupleVal([]cty.Value{
   965  					fooBlockValue,
   966  					fooBlockValue,
   967  				}),
   968  			}),
   969  			cty.ObjectVal(map[string]cty.Value{
   970  				"key": cty.TupleVal([]cty.Value{
   971  					fooBlockValue,
   972  				}),
   973  			}),
   974  			[]string{
   975  				`.key: block count changed from 2 to 1`,
   976  			},
   977  		},
   978  		{
   979  			&configschema.Block{
   980  				BlockTypes: map[string]*configschema.NestedBlock{
   981  					"key": {
   982  						Nesting: configschema.NestingList,
   983  						Block:   schemaWithFoo,
   984  					},
   985  				},
   986  			},
   987  			cty.ObjectVal(map[string]cty.Value{
   988  				"key": cty.TupleVal([]cty.Value{}),
   989  			}),
   990  			cty.ObjectVal(map[string]cty.Value{
   991  				"key": cty.TupleVal([]cty.Value{
   992  					fooBlockValue,
   993  					fooBlockValue,
   994  				}),
   995  			}),
   996  			[]string{
   997  				`.key: block count changed from 0 to 2`,
   998  			},
   999  		},
  1000  		{
  1001  			&configschema.Block{
  1002  				BlockTypes: map[string]*configschema.NestedBlock{
  1003  					"key": {
  1004  						Nesting: configschema.NestingList,
  1005  						Block:   schemaWithFooBar,
  1006  					},
  1007  				},
  1008  			},
  1009  			cty.UnknownVal(cty.Object(map[string]cty.Type{
  1010  				"key": cty.List(fooBarBlockValue.Type()),
  1011  			})),
  1012  			cty.ObjectVal(map[string]cty.Value{
  1013  				"key": cty.ListVal([]cty.Value{
  1014  					cty.ObjectVal(map[string]cty.Value{
  1015  						"foo": cty.StringVal("hello"),
  1016  					}),
  1017  					cty.ObjectVal(map[string]cty.Value{
  1018  						"foo": cty.StringVal("world"),
  1019  					}),
  1020  				}),
  1021  			}),
  1022  			nil, // an unknown block is allowed to expand into multiple, because that's how dynamic blocks behave when for_each is unknown
  1023  		},
  1024  		{
  1025  			&configschema.Block{
  1026  				BlockTypes: map[string]*configschema.NestedBlock{
  1027  					"key": {
  1028  						Nesting: configschema.NestingList,
  1029  						Block:   schemaWithFooBar,
  1030  					},
  1031  				},
  1032  			},
  1033  			// While we must make an exception for empty strings in sets due to
  1034  			// the legacy SDK, lists should be compared more strictly.
  1035  			// This does not count as a dynamic block placeholder
  1036  			cty.ObjectVal(map[string]cty.Value{
  1037  				"key": cty.ListVal([]cty.Value{
  1038  					fooBarBlockValue,
  1039  					cty.ObjectVal(map[string]cty.Value{
  1040  						"foo": cty.UnknownVal(cty.String),
  1041  						"bar": cty.StringVal(""),
  1042  					}),
  1043  				}),
  1044  			}),
  1045  			cty.ObjectVal(map[string]cty.Value{
  1046  				"key": cty.ListVal([]cty.Value{
  1047  					fooBlockValue,
  1048  					cty.ObjectVal(map[string]cty.Value{
  1049  						"foo": cty.StringVal("hello"),
  1050  					}),
  1051  					cty.ObjectVal(map[string]cty.Value{
  1052  						"foo": cty.StringVal("world"),
  1053  					}),
  1054  				}),
  1055  			}),
  1056  			[]string{".key: block count changed from 2 to 3"},
  1057  		},
  1058  
  1059  		// NestingSet blocks
  1060  		{
  1061  			&configschema.Block{
  1062  				BlockTypes: map[string]*configschema.NestedBlock{
  1063  					"block": {
  1064  						Nesting: configschema.NestingSet,
  1065  						Block:   schemaWithFoo,
  1066  					},
  1067  				},
  1068  			},
  1069  			cty.ObjectVal(map[string]cty.Value{
  1070  				"block": cty.SetVal([]cty.Value{
  1071  					cty.ObjectVal(map[string]cty.Value{
  1072  						"foo": cty.StringVal("hello"),
  1073  					}),
  1074  					cty.ObjectVal(map[string]cty.Value{
  1075  						"foo": cty.StringVal("world"),
  1076  					}),
  1077  				}),
  1078  			}),
  1079  			cty.ObjectVal(map[string]cty.Value{
  1080  				"block": cty.SetVal([]cty.Value{
  1081  					cty.ObjectVal(map[string]cty.Value{
  1082  						"foo": cty.StringVal("hello"),
  1083  					}),
  1084  					cty.ObjectVal(map[string]cty.Value{
  1085  						"foo": cty.StringVal("world"),
  1086  					}),
  1087  				}),
  1088  			}),
  1089  			nil,
  1090  		},
  1091  		{
  1092  			&configschema.Block{
  1093  				BlockTypes: map[string]*configschema.NestedBlock{
  1094  					"block": {
  1095  						Nesting: configschema.NestingSet,
  1096  						Block:   schemaWithFoo,
  1097  					},
  1098  				},
  1099  			},
  1100  			cty.ObjectVal(map[string]cty.Value{
  1101  				"block": cty.SetVal([]cty.Value{
  1102  					cty.ObjectVal(map[string]cty.Value{
  1103  						"foo": cty.UnknownVal(cty.String),
  1104  					}),
  1105  					cty.ObjectVal(map[string]cty.Value{
  1106  						"foo": cty.UnknownVal(cty.String),
  1107  					}),
  1108  				}),
  1109  			}),
  1110  			cty.ObjectVal(map[string]cty.Value{
  1111  				"block": cty.SetVal([]cty.Value{
  1112  					// This is testing the scenario where the two unknown values
  1113  					// turned out to be equal after we learned their values,
  1114  					// and so they coalesced together into a single element.
  1115  					cty.ObjectVal(map[string]cty.Value{
  1116  						"foo": cty.StringVal("hello"),
  1117  					}),
  1118  				}),
  1119  			}),
  1120  			nil,
  1121  		},
  1122  		{
  1123  			&configschema.Block{
  1124  				BlockTypes: map[string]*configschema.NestedBlock{
  1125  					"block": {
  1126  						Nesting: configschema.NestingSet,
  1127  						Block:   schemaWithFoo,
  1128  					},
  1129  				},
  1130  			},
  1131  			cty.ObjectVal(map[string]cty.Value{
  1132  				"block": cty.SetVal([]cty.Value{
  1133  					cty.ObjectVal(map[string]cty.Value{
  1134  						"foo": cty.UnknownVal(cty.String),
  1135  					}),
  1136  					cty.ObjectVal(map[string]cty.Value{
  1137  						"foo": cty.UnknownVal(cty.String),
  1138  					}),
  1139  				}),
  1140  			}),
  1141  			cty.ObjectVal(map[string]cty.Value{
  1142  				"block": cty.SetVal([]cty.Value{
  1143  					cty.ObjectVal(map[string]cty.Value{
  1144  						"foo": cty.StringVal("hello"),
  1145  					}),
  1146  					cty.ObjectVal(map[string]cty.Value{
  1147  						"foo": cty.StringVal("world"),
  1148  					}),
  1149  				}),
  1150  			}),
  1151  			nil,
  1152  		},
  1153  		{
  1154  			&configschema.Block{
  1155  				BlockTypes: map[string]*configschema.NestedBlock{
  1156  					"block": {
  1157  						Nesting: configschema.NestingSet,
  1158  						Block:   schemaWithFoo,
  1159  					},
  1160  				},
  1161  			},
  1162  			cty.ObjectVal(map[string]cty.Value{
  1163  				"block": cty.UnknownVal(cty.Set(
  1164  					cty.Object(map[string]cty.Type{
  1165  						"foo": cty.String,
  1166  					}),
  1167  				)),
  1168  			}),
  1169  			cty.ObjectVal(map[string]cty.Value{
  1170  				"block": cty.SetVal([]cty.Value{
  1171  					cty.ObjectVal(map[string]cty.Value{
  1172  						"foo": cty.StringVal("hello"),
  1173  					}),
  1174  					cty.ObjectVal(map[string]cty.Value{
  1175  						"foo": cty.StringVal("world"),
  1176  					}),
  1177  					cty.ObjectVal(map[string]cty.Value{
  1178  						"foo": cty.StringVal("nope"),
  1179  					}),
  1180  				}),
  1181  			}),
  1182  			// there is no error here, because the presence of unknowns
  1183  			// indicates this may be a dynamic block, and the length is unknown
  1184  			nil,
  1185  		},
  1186  		{
  1187  			&configschema.Block{
  1188  				BlockTypes: map[string]*configschema.NestedBlock{
  1189  					"block": {
  1190  						Nesting: configschema.NestingSet,
  1191  						Block:   schemaWithFoo,
  1192  					},
  1193  				},
  1194  			},
  1195  			cty.ObjectVal(map[string]cty.Value{
  1196  				"block": cty.SetVal([]cty.Value{
  1197  					cty.ObjectVal(map[string]cty.Value{
  1198  						"foo": cty.StringVal("hello"),
  1199  					}),
  1200  					cty.ObjectVal(map[string]cty.Value{
  1201  						"foo": cty.StringVal("world"),
  1202  					}),
  1203  				}),
  1204  			}),
  1205  			cty.ObjectVal(map[string]cty.Value{
  1206  				"block": cty.SetVal([]cty.Value{
  1207  					cty.ObjectVal(map[string]cty.Value{
  1208  						"foo": cty.StringVal("howdy"),
  1209  					}),
  1210  					cty.ObjectVal(map[string]cty.Value{
  1211  						"foo": cty.StringVal("world"),
  1212  					}),
  1213  				}),
  1214  			}),
  1215  			[]string{
  1216  				`.block: planned set element cty.ObjectVal(map[string]cty.Value{"foo":cty.StringVal("hello")}) does not correlate with any element in actual`,
  1217  			},
  1218  		},
  1219  		{
  1220  			// This one is an odd situation where the value representing the
  1221  			// block itself is unknown. This is never supposed to be true,
  1222  			// but in legacy SDK mode we allow such things to pass through as
  1223  			// a warning, and so we must tolerate them for matching purposes.
  1224  			&configschema.Block{
  1225  				BlockTypes: map[string]*configschema.NestedBlock{
  1226  					"block": {
  1227  						Nesting: configschema.NestingSet,
  1228  						Block:   schemaWithFoo,
  1229  					},
  1230  				},
  1231  			},
  1232  			cty.ObjectVal(map[string]cty.Value{
  1233  				"block": cty.SetVal([]cty.Value{
  1234  					cty.ObjectVal(map[string]cty.Value{
  1235  						"foo": cty.UnknownVal(cty.String),
  1236  					}),
  1237  					cty.ObjectVal(map[string]cty.Value{
  1238  						"foo": cty.UnknownVal(cty.String),
  1239  					}),
  1240  				}),
  1241  			}),
  1242  			cty.ObjectVal(map[string]cty.Value{
  1243  				"block": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
  1244  					"foo": cty.String,
  1245  				}))),
  1246  			}),
  1247  			nil,
  1248  		},
  1249  		{
  1250  			&configschema.Block{
  1251  				BlockTypes: map[string]*configschema.NestedBlock{
  1252  					"block": {
  1253  						Nesting: configschema.NestingSet,
  1254  						Block:   schemaWithFoo,
  1255  					},
  1256  				},
  1257  			},
  1258  			cty.ObjectVal(map[string]cty.Value{
  1259  				"block": cty.UnknownVal(cty.Set(fooBlockValue.Type())),
  1260  			}),
  1261  			cty.ObjectVal(map[string]cty.Value{
  1262  				"block": cty.SetVal([]cty.Value{
  1263  					cty.ObjectVal(map[string]cty.Value{
  1264  						"foo": cty.StringVal("a"),
  1265  					}),
  1266  					cty.ObjectVal(map[string]cty.Value{
  1267  						"foo": cty.StringVal("b"),
  1268  					}),
  1269  				}),
  1270  			}),
  1271  			nil,
  1272  		},
  1273  		// test a set with an unknown dynamic count going to 0 values
  1274  		{
  1275  			&configschema.Block{
  1276  				BlockTypes: map[string]*configschema.NestedBlock{
  1277  					"block2": {
  1278  						Nesting: configschema.NestingSet,
  1279  						Block:   schemaWithFoo,
  1280  					},
  1281  				},
  1282  			},
  1283  			cty.ObjectVal(map[string]cty.Value{
  1284  				"block2": cty.UnknownVal(cty.Set(fooBlockValue.Type())),
  1285  			}),
  1286  			cty.ObjectVal(map[string]cty.Value{
  1287  				"block2": cty.SetValEmpty(cty.Object(map[string]cty.Type{
  1288  					"foo": cty.String,
  1289  				})),
  1290  			}),
  1291  			nil,
  1292  		},
  1293  		// test a set with a patially known dynamic count reducing it's values
  1294  		{
  1295  			&configschema.Block{
  1296  				BlockTypes: map[string]*configschema.NestedBlock{
  1297  					"block3": {
  1298  						Nesting: configschema.NestingSet,
  1299  						Block:   schemaWithFoo,
  1300  					},
  1301  				},
  1302  			},
  1303  			cty.ObjectVal(map[string]cty.Value{
  1304  				"block3": cty.SetVal([]cty.Value{
  1305  					cty.ObjectVal(map[string]cty.Value{
  1306  						"foo": cty.StringVal("a"),
  1307  					}),
  1308  					cty.ObjectVal(map[string]cty.Value{
  1309  						"foo": cty.UnknownVal(cty.String),
  1310  					}),
  1311  				}),
  1312  			}),
  1313  			cty.ObjectVal(map[string]cty.Value{
  1314  				"block3": cty.SetVal([]cty.Value{
  1315  					cty.ObjectVal(map[string]cty.Value{
  1316  						"foo": cty.StringVal("a"),
  1317  					}),
  1318  				}),
  1319  			}),
  1320  			nil,
  1321  		},
  1322  		{
  1323  			&configschema.Block{
  1324  				BlockTypes: map[string]*configschema.NestedBlock{
  1325  					"block": {
  1326  						Nesting: configschema.NestingList,
  1327  						Block: configschema.Block{
  1328  							Attributes: map[string]*configschema.Attribute{
  1329  								"foo": {
  1330  									Type:     cty.String,
  1331  									Required: true,
  1332  								},
  1333  							},
  1334  						},
  1335  					},
  1336  				},
  1337  			},
  1338  			cty.ObjectVal(map[string]cty.Value{
  1339  				"block": cty.EmptyObjectVal,
  1340  			}),
  1341  			cty.ObjectVal(map[string]cty.Value{
  1342  				"block": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
  1343  					"foo": cty.String,
  1344  				}))),
  1345  			}),
  1346  			nil,
  1347  		},
  1348  	}
  1349  
  1350  	for i, test := range tests {
  1351  		t.Run(fmt.Sprintf("%02d: %#v and %#v", i, test.Planned, test.Actual), func(t *testing.T) {
  1352  			errs := AssertObjectCompatible(test.Schema, test.Planned, test.Actual)
  1353  
  1354  			wantErrs := make(map[string]struct{})
  1355  			gotErrs := make(map[string]struct{})
  1356  			for _, err := range errs {
  1357  				gotErrs[tfdiags.FormatError(err)] = struct{}{}
  1358  			}
  1359  			for _, msg := range test.WantErrs {
  1360  				wantErrs[msg] = struct{}{}
  1361  			}
  1362  
  1363  			t.Logf("\nplanned: %sactual:  %s", dump.Value(test.Planned), dump.Value(test.Actual))
  1364  			for msg := range wantErrs {
  1365  				if _, ok := gotErrs[msg]; !ok {
  1366  					t.Errorf("missing expected error: %s", msg)
  1367  				}
  1368  			}
  1369  			for msg := range gotErrs {
  1370  				if _, ok := wantErrs[msg]; !ok {
  1371  					t.Errorf("unexpected extra error: %s", msg)
  1372  				}
  1373  			}
  1374  		})
  1375  	}
  1376  }