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