github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/plans/planfile/planfile_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package planfile
     5  
     6  import (
     7  	"path/filepath"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/google/go-cmp/cmp"
    12  
    13  	"github.com/terramate-io/tf/addrs"
    14  	"github.com/terramate-io/tf/configs/configload"
    15  	"github.com/terramate-io/tf/depsfile"
    16  	"github.com/terramate-io/tf/getproviders"
    17  	"github.com/terramate-io/tf/plans"
    18  	"github.com/terramate-io/tf/states"
    19  	"github.com/terramate-io/tf/states/statefile"
    20  	tfversion "github.com/terramate-io/tf/version"
    21  )
    22  
    23  func TestRoundtrip(t *testing.T) {
    24  	fixtureDir := filepath.Join("testdata", "test-config")
    25  	loader, err := configload.NewLoader(&configload.Config{
    26  		ModulesDir: filepath.Join(fixtureDir, ".terraform", "modules"),
    27  	})
    28  	if err != nil {
    29  		t.Fatal(err)
    30  	}
    31  
    32  	_, snapIn, diags := loader.LoadConfigWithSnapshot(fixtureDir)
    33  	if diags.HasErrors() {
    34  		t.Fatal(diags.Error())
    35  	}
    36  
    37  	// Just a minimal state file so we can test that it comes out again at all.
    38  	// We don't need to test the entire thing because the state file
    39  	// serialization is already tested in its own package.
    40  	stateFileIn := &statefile.File{
    41  		TerraformVersion: tfversion.SemVer,
    42  		Serial:           2,
    43  		Lineage:          "abc123",
    44  		State:            states.NewState(),
    45  	}
    46  	prevStateFileIn := &statefile.File{
    47  		TerraformVersion: tfversion.SemVer,
    48  		Serial:           1,
    49  		Lineage:          "abc123",
    50  		State:            states.NewState(),
    51  	}
    52  
    53  	// Minimal plan too, since the serialization of the tfplan portion of the
    54  	// file is tested more fully in tfplan_test.go .
    55  	planIn := &plans.Plan{
    56  		Changes: &plans.Changes{
    57  			Resources: []*plans.ResourceInstanceChangeSrc{},
    58  			Outputs:   []*plans.OutputChangeSrc{},
    59  		},
    60  		DriftedResources: []*plans.ResourceInstanceChangeSrc{},
    61  		VariableValues: map[string]plans.DynamicValue{
    62  			"foo": plans.DynamicValue([]byte("foo placeholder")),
    63  		},
    64  		Backend: plans.Backend{
    65  			Type:      "local",
    66  			Config:    plans.DynamicValue([]byte("config placeholder")),
    67  			Workspace: "default",
    68  		},
    69  		Checks: &states.CheckResults{},
    70  
    71  		// Due to some historical oddities in how we've changed modelling over
    72  		// time, we also include the states (without the corresponding file
    73  		// headers) in the plans.Plan object. This is currently ignored by
    74  		// Create but will be returned by ReadPlan and so we need to include
    75  		// it here so that we'll get a match when we compare input and output
    76  		// below.
    77  		PrevRunState: prevStateFileIn.State,
    78  		PriorState:   stateFileIn.State,
    79  	}
    80  
    81  	locksIn := depsfile.NewLocks()
    82  	locksIn.SetProvider(
    83  		addrs.NewDefaultProvider("boop"),
    84  		getproviders.MustParseVersion("1.0.0"),
    85  		getproviders.MustParseVersionConstraints(">= 1.0.0"),
    86  		[]getproviders.Hash{
    87  			getproviders.MustParseHash("fake:hello"),
    88  		},
    89  	)
    90  
    91  	planFn := filepath.Join(t.TempDir(), "tfplan")
    92  
    93  	err = Create(planFn, CreateArgs{
    94  		ConfigSnapshot:       snapIn,
    95  		PreviousRunStateFile: prevStateFileIn,
    96  		StateFile:            stateFileIn,
    97  		Plan:                 planIn,
    98  		DependencyLocks:      locksIn,
    99  	})
   100  	if err != nil {
   101  		t.Fatalf("failed to create plan file: %s", err)
   102  	}
   103  
   104  	wpf, err := OpenWrapped(planFn)
   105  	if err != nil {
   106  		t.Fatalf("failed to open plan file for reading: %s", err)
   107  	}
   108  	pr, ok := wpf.Local()
   109  	if !ok {
   110  		t.Fatalf("failed to open plan file as a local plan file")
   111  	}
   112  	if wpf.IsCloud() {
   113  		t.Fatalf("wrapped plan claims to be both kinds of plan at once")
   114  	}
   115  
   116  	t.Run("ReadPlan", func(t *testing.T) {
   117  		planOut, err := pr.ReadPlan()
   118  		if err != nil {
   119  			t.Fatalf("failed to read plan: %s", err)
   120  		}
   121  		if diff := cmp.Diff(planIn, planOut); diff != "" {
   122  			t.Errorf("plan did not survive round-trip\n%s", diff)
   123  		}
   124  	})
   125  
   126  	t.Run("ReadStateFile", func(t *testing.T) {
   127  		stateFileOut, err := pr.ReadStateFile()
   128  		if err != nil {
   129  			t.Fatalf("failed to read state: %s", err)
   130  		}
   131  		if diff := cmp.Diff(stateFileIn, stateFileOut); diff != "" {
   132  			t.Errorf("state file did not survive round-trip\n%s", diff)
   133  		}
   134  	})
   135  
   136  	t.Run("ReadPrevStateFile", func(t *testing.T) {
   137  		prevStateFileOut, err := pr.ReadPrevStateFile()
   138  		if err != nil {
   139  			t.Fatalf("failed to read state: %s", err)
   140  		}
   141  		if diff := cmp.Diff(prevStateFileIn, prevStateFileOut); diff != "" {
   142  			t.Errorf("state file did not survive round-trip\n%s", diff)
   143  		}
   144  	})
   145  
   146  	t.Run("ReadConfigSnapshot", func(t *testing.T) {
   147  		snapOut, err := pr.ReadConfigSnapshot()
   148  		if err != nil {
   149  			t.Fatalf("failed to read config snapshot: %s", err)
   150  		}
   151  		if diff := cmp.Diff(snapIn, snapOut); diff != "" {
   152  			t.Errorf("config snapshot did not survive round-trip\n%s", diff)
   153  		}
   154  	})
   155  
   156  	t.Run("ReadConfig", func(t *testing.T) {
   157  		// Reading from snapshots is tested in the configload package, so
   158  		// here we'll just test that we can successfully do it, to see if the
   159  		// glue code in _this_ package is correct.
   160  		_, diags := pr.ReadConfig()
   161  		if diags.HasErrors() {
   162  			t.Errorf("when reading config: %s", diags.Err())
   163  		}
   164  	})
   165  
   166  	t.Run("ReadDependencyLocks", func(t *testing.T) {
   167  		locksOut, diags := pr.ReadDependencyLocks()
   168  		if diags.HasErrors() {
   169  			t.Fatalf("when reading config: %s", diags.Err())
   170  		}
   171  		got := locksOut.AllProviders()
   172  		want := locksIn.AllProviders()
   173  		if diff := cmp.Diff(want, got, cmp.AllowUnexported(depsfile.ProviderLock{})); diff != "" {
   174  			t.Errorf("provider locks did not survive round-trip\n%s", diff)
   175  		}
   176  	})
   177  }
   178  
   179  func TestWrappedError(t *testing.T) {
   180  	// Open something that isn't a cloud or local planfile: should error
   181  	wrongFile := "not a valid zip file"
   182  	_, err := OpenWrapped(filepath.Join("testdata", "test-config", "root.tf"))
   183  	if !strings.Contains(err.Error(), wrongFile) {
   184  		t.Fatalf("expected  %q, got %q", wrongFile, err)
   185  	}
   186  
   187  	// Open something that doesn't exist: should error
   188  	missingFile := "no such file or directory"
   189  	_, err = OpenWrapped(filepath.Join("testdata", "absent.tfplan"))
   190  	if !strings.Contains(err.Error(), missingFile) {
   191  		t.Fatalf("expected  %q, got %q", missingFile, err)
   192  	}
   193  }
   194  
   195  func TestWrappedCloud(t *testing.T) {
   196  	// Loading valid cloud plan results in a wrapped cloud plan
   197  	wpf, err := OpenWrapped(filepath.Join("testdata", "cloudplan.json"))
   198  	if err != nil {
   199  		t.Fatalf("failed to open valid cloud plan: %s", err)
   200  	}
   201  	if !wpf.IsCloud() {
   202  		t.Fatalf("failed to open cloud file as a cloud plan")
   203  	}
   204  	if wpf.IsLocal() {
   205  		t.Fatalf("wrapped plan claims to be both kinds of plan at once")
   206  	}
   207  }