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 }