github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/configupgrade/upgrade_test.go (about)

     1  package configupgrade
     2  
     3  import (
     4  	"bytes"
     5  	"flag"
     6  	"io"
     7  	"io/ioutil"
     8  	"log"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"testing"
    13  
    14  	"github.com/davecgh/go-spew/spew"
    15  	"github.com/zclconf/go-cty/cty"
    16  
    17  	"github.com/hashicorp/terraform/addrs"
    18  	backendinit "github.com/hashicorp/terraform/backend/init"
    19  	"github.com/hashicorp/terraform/configs/configschema"
    20  	"github.com/hashicorp/terraform/helper/logging"
    21  	"github.com/hashicorp/terraform/providers"
    22  	"github.com/hashicorp/terraform/provisioners"
    23  	"github.com/hashicorp/terraform/terraform"
    24  )
    25  
    26  func TestUpgradeValid(t *testing.T) {
    27  	// This test uses the contents of the testdata/valid directory as
    28  	// a table of tests. Every directory there must have both "input" and
    29  	// "want" subdirectories, where "input" is the configuration to be
    30  	// upgraded and "want" is the expected result.
    31  	fixtureDir := "testdata/valid"
    32  	testDirs, err := ioutil.ReadDir(fixtureDir)
    33  	if err != nil {
    34  		t.Fatal(err)
    35  	}
    36  
    37  	for _, entry := range testDirs {
    38  		if !entry.IsDir() {
    39  			continue
    40  		}
    41  		t.Run(entry.Name(), func(t *testing.T) {
    42  			inputDir := filepath.Join(fixtureDir, entry.Name(), "input")
    43  			wantDir := filepath.Join(fixtureDir, entry.Name(), "want")
    44  			u := &Upgrader{
    45  				Providers:    providers.ResolverFixed(testProviders),
    46  				Provisioners: testProvisioners,
    47  			}
    48  
    49  			inputSrc, err := LoadModule(inputDir)
    50  			if err != nil {
    51  				t.Fatal(err)
    52  			}
    53  			wantSrc, err := LoadModule(wantDir)
    54  			if err != nil {
    55  				t.Fatal(err)
    56  			}
    57  
    58  			gotSrc, diags := u.Upgrade(inputSrc, inputDir)
    59  			if diags.HasErrors() {
    60  				t.Error(diags.Err())
    61  			}
    62  
    63  			// Upgrade uses a nil entry as a signal to delete a file, which
    64  			// we can't test here because we aren't modifying an existing
    65  			// dir in place, so we'll just ignore those and leave that mechanism
    66  			// to be tested elsewhere.
    67  
    68  			for name, got := range gotSrc {
    69  				if gotSrc[name] == nil {
    70  					delete(gotSrc, name)
    71  					continue
    72  				}
    73  				want, wanted := wantSrc[name]
    74  				if !wanted {
    75  					t.Errorf("unexpected extra output file %q\n=== GOT ===\n%s", name, got)
    76  					continue
    77  				}
    78  
    79  				got = bytes.TrimSpace(got)
    80  				want = bytes.TrimSpace(want)
    81  				if !bytes.Equal(got, want) {
    82  					diff := diffSourceFiles(got, want)
    83  					t.Errorf("wrong content in %q\n%s", name, diff)
    84  				}
    85  			}
    86  
    87  			for name, want := range wantSrc {
    88  				if _, present := gotSrc[name]; !present {
    89  					t.Errorf("missing output file %q\n=== WANT ===\n%s", name, want)
    90  				}
    91  			}
    92  		})
    93  	}
    94  }
    95  
    96  func TestUpgradeRenameJSON(t *testing.T) {
    97  	inputDir := filepath.Join("testdata/valid/rename-json/input")
    98  	inputSrc, err := LoadModule(inputDir)
    99  	if err != nil {
   100  		t.Fatal(err)
   101  	}
   102  
   103  	u := &Upgrader{
   104  		Providers: providers.ResolverFixed(testProviders),
   105  	}
   106  	gotSrc, diags := u.Upgrade(inputSrc, inputDir)
   107  	if diags.HasErrors() {
   108  		t.Error(diags.Err())
   109  	}
   110  
   111  	// This test fixture is also fully covered by TestUpgradeValid, so
   112  	// we're just testing that the file was renamed here.
   113  	src, exists := gotSrc["misnamed-json.tf"]
   114  	if src != nil {
   115  		t.Errorf("misnamed-json.tf still has content")
   116  	} else if !exists {
   117  		t.Errorf("misnamed-json.tf not marked for deletion")
   118  	}
   119  
   120  	src, exists = gotSrc["misnamed-json.tf.json"]
   121  	if src == nil || !exists {
   122  		t.Errorf("misnamed-json.tf.json was not created")
   123  	}
   124  }
   125  
   126  func diffSourceFiles(got, want []byte) []byte {
   127  	// We'll try to run "diff -u" here to get nice output, but if that fails
   128  	// (e.g. because we're running on a machine without diff installed) then
   129  	// we'll fall back on just printing out the before and after in full.
   130  	gotR, gotW, err := os.Pipe()
   131  	if err != nil {
   132  		return diffSourceFilesFallback(got, want)
   133  	}
   134  	defer gotR.Close()
   135  	defer gotW.Close()
   136  	wantR, wantW, err := os.Pipe()
   137  	if err != nil {
   138  		return diffSourceFilesFallback(got, want)
   139  	}
   140  	defer wantR.Close()
   141  	defer wantW.Close()
   142  
   143  	cmd := exec.Command("diff", "-u", "--label=GOT", "--label=WANT", "/dev/fd/3", "/dev/fd/4")
   144  	cmd.ExtraFiles = []*os.File{gotR, wantR}
   145  	stdout, err := cmd.StdoutPipe()
   146  	stderr, err := cmd.StderrPipe()
   147  	if err != nil {
   148  		return diffSourceFilesFallback(got, want)
   149  	}
   150  
   151  	go func() {
   152  		wantW.Write(want)
   153  		wantW.Close()
   154  	}()
   155  	go func() {
   156  		gotW.Write(got)
   157  		gotW.Close()
   158  	}()
   159  
   160  	err = cmd.Start()
   161  	if err != nil {
   162  		return diffSourceFilesFallback(got, want)
   163  	}
   164  
   165  	outR := io.MultiReader(stdout, stderr)
   166  	out, err := ioutil.ReadAll(outR)
   167  	if err != nil {
   168  		return diffSourceFilesFallback(got, want)
   169  	}
   170  
   171  	cmd.Wait() // not checking errors here because on failure we'll have stderr captured to return
   172  
   173  	const noNewline = "\\ No newline at end of file\n"
   174  	if bytes.HasSuffix(out, []byte(noNewline)) {
   175  		out = out[:len(out)-len(noNewline)]
   176  	}
   177  	return out
   178  }
   179  
   180  func diffSourceFilesFallback(got, want []byte) []byte {
   181  	var buf bytes.Buffer
   182  	buf.WriteString("=== GOT ===\n")
   183  	buf.Write(got)
   184  	buf.WriteString("\n=== WANT ===\n")
   185  	buf.Write(want)
   186  	buf.WriteString("\n")
   187  	return buf.Bytes()
   188  }
   189  
   190  var testProviders = map[addrs.Provider]providers.Factory{
   191  	addrs.NewLegacyProvider("test"): providers.Factory(func() (providers.Interface, error) {
   192  		p := &terraform.MockProvider{}
   193  		p.GetSchemaReturn = &terraform.ProviderSchema{
   194  			ResourceTypes: map[string]*configschema.Block{
   195  				"test_instance": {
   196  					Attributes: map[string]*configschema.Attribute{
   197  						"id":              {Type: cty.String, Computed: true},
   198  						"type":            {Type: cty.String, Optional: true},
   199  						"image":           {Type: cty.String, Optional: true},
   200  						"tags":            {Type: cty.Map(cty.String), Optional: true},
   201  						"security_groups": {Type: cty.List(cty.String), Optional: true},
   202  						"subnet_ids":      {Type: cty.Set(cty.String), Optional: true},
   203  						"list_of_obj":     {Type: cty.List(cty.EmptyObject), Optional: true},
   204  					},
   205  					BlockTypes: map[string]*configschema.NestedBlock{
   206  						"network": {
   207  							Nesting: configschema.NestingSet,
   208  							Block: configschema.Block{
   209  								Attributes: map[string]*configschema.Attribute{
   210  									"cidr_block":   {Type: cty.String, Optional: true},
   211  									"subnet_cidrs": {Type: cty.Map(cty.String), Computed: true},
   212  								},
   213  								BlockTypes: map[string]*configschema.NestedBlock{
   214  									"subnet": {
   215  										Nesting: configschema.NestingSet,
   216  										Block: configschema.Block{
   217  											Attributes: map[string]*configschema.Attribute{
   218  												"number": {Type: cty.Number, Required: true},
   219  											},
   220  										},
   221  									},
   222  								},
   223  							},
   224  						},
   225  						"addresses": {
   226  							Nesting: configschema.NestingSingle,
   227  							Block: configschema.Block{
   228  								Attributes: map[string]*configschema.Attribute{
   229  									"ipv4": {Type: cty.String, Computed: true},
   230  									"ipv6": {Type: cty.String, Computed: true},
   231  								},
   232  							},
   233  						},
   234  					},
   235  				},
   236  			},
   237  		}
   238  		return p, nil
   239  	}),
   240  	addrs.NewLegacyProvider("terraform"): providers.Factory(func() (providers.Interface, error) {
   241  		p := &terraform.MockProvider{}
   242  		p.GetSchemaReturn = &terraform.ProviderSchema{
   243  			DataSources: map[string]*configschema.Block{
   244  				"terraform_remote_state": {
   245  					// This is just enough an approximation of the remote state
   246  					// schema to check out reference upgrade logic. It is
   247  					// intentionally not fully-comprehensive.
   248  					Attributes: map[string]*configschema.Attribute{
   249  						"backend": {Type: cty.String, Optional: true},
   250  					},
   251  				},
   252  			},
   253  		}
   254  		return p, nil
   255  	}),
   256  	addrs.NewLegacyProvider("aws"): providers.Factory(func() (providers.Interface, error) {
   257  		// This is here only so we can test the provisioner connection info
   258  		// migration behavior, which is resource-type specific. Do not use
   259  		// it in any other tests.
   260  		p := &terraform.MockProvider{}
   261  		p.GetSchemaReturn = &terraform.ProviderSchema{
   262  			ResourceTypes: map[string]*configschema.Block{
   263  				"aws_instance": {},
   264  			},
   265  		}
   266  		return p, nil
   267  	}),
   268  }
   269  
   270  var testProvisioners = map[string]provisioners.Factory{
   271  	"test": provisioners.Factory(func() (provisioners.Interface, error) {
   272  		p := &terraform.MockProvisioner{}
   273  		p.GetSchemaResponse = provisioners.GetSchemaResponse{
   274  			Provisioner: &configschema.Block{
   275  				Attributes: map[string]*configschema.Attribute{
   276  					"commands":    {Type: cty.List(cty.String), Optional: true},
   277  					"interpreter": {Type: cty.String, Optional: true},
   278  				},
   279  			},
   280  		}
   281  		return p, nil
   282  	}),
   283  }
   284  
   285  func init() {
   286  	// Initialize the backends
   287  	backendinit.Init(nil)
   288  }
   289  
   290  func TestMain(m *testing.M) {
   291  	flag.Parse()
   292  	if testing.Verbose() {
   293  		// if we're verbose, use the logging requested by TF_LOG
   294  		logging.SetOutput()
   295  	} else {
   296  		// otherwise silence all logs
   297  		log.SetOutput(ioutil.Discard)
   298  	}
   299  
   300  	// We have fmt.Stringer implementations on lots of objects that hide
   301  	// details that we very often want to see in tests, so we just disable
   302  	// spew's use of String methods globally on the assumption that spew
   303  	// usage implies an intent to see the raw values and ignore any
   304  	// abstractions.
   305  	spew.Config.DisableMethods = true
   306  
   307  	os.Exit(m.Run())
   308  }