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 }