-
Notifications
You must be signed in to change notification settings - Fork 10
/
reader.go
242 lines (217 loc) · 8.09 KB
/
reader.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package planfile
import (
"archive/zip"
"bytes"
"fmt"
"io/ioutil"
"github.com/snyk/policy-engine/pkg/internal/terraform/configs"
"github.com/snyk/policy-engine/pkg/internal/terraform/configs/configload"
"github.com/snyk/policy-engine/pkg/internal/terraform/depsfile"
"github.com/snyk/policy-engine/pkg/internal/terraform/plans"
"github.com/snyk/policy-engine/pkg/internal/terraform/states/statefile"
"github.com/snyk/policy-engine/pkg/internal/terraform/tfdiags"
)
const tfstateFilename = "tfstate"
const tfstatePreviousFilename = "tfstate-prev"
const dependencyLocksFilename = ".terraform.lock.hcl" // matches the conventional name in an input configuration
// Reader is the main type used to read plan files. Create a Reader by calling
// Open.
//
// A plan file is a random-access file format, so methods of Reader must
// be used to access the individual portions of the file for further
// processing.
type Reader struct {
zip *zip.ReadCloser
}
// Open creates a Reader for the file at the given filename, or returns an
// error if the file doesn't seem to be a planfile.
func Open(filename string) (*Reader, error) {
r, err := zip.OpenReader(filename)
if err != nil {
// To give a better error message, we'll sniff to see if this looks
// like our old plan format from versions prior to 0.12.
if b, sErr := ioutil.ReadFile(filename); sErr == nil {
if bytes.HasPrefix(b, []byte("tfplan")) {
return nil, fmt.Errorf("the given plan file was created by an earlier version of Terraform; plan files cannot be shared between different Terraform versions")
}
}
return nil, err
}
// Sniff to make sure this looks like a plan file, as opposed to any other
// random zip file the user might have around.
var planFile *zip.File
for _, file := range r.File {
if file.Name == tfplanFilename {
planFile = file
break
}
}
if planFile == nil {
return nil, fmt.Errorf("the given file is not a valid plan file")
}
// For now, we'll just accept the presence of the tfplan file as enough,
// and wait to validate the version when the caller requests the plan
// itself.
return &Reader{
zip: r,
}, nil
}
// ReadPlan reads the plan embedded in the plan file.
//
// Errors can be returned for various reasons, including if the plan file
// is not of an appropriate format version, if it was created by a different
// version of Terraform, if it is invalid, etc.
func (r *Reader) ReadPlan() (*plans.Plan, error) {
var planFile *zip.File
for _, file := range r.zip.File {
if file.Name == tfplanFilename {
planFile = file
break
}
}
if planFile == nil {
// This should never happen because we checked for this file during
// Open, but we'll check anyway to be safe.
return nil, fmt.Errorf("the plan file is invalid")
}
pr, err := planFile.Open()
if err != nil {
return nil, fmt.Errorf("failed to retrieve plan from plan file: %s", err)
}
defer pr.Close()
// There's a slight mismatch in how plans.Plan is modeled vs. how
// the underlying plan file format works, because the "tfplan" embedded
// file contains only some top-level metadata and the planned changes,
// and not the previous run or prior states. Therefore we need to
// build this up in multiple steps.
// This is some technical debt because historically we considered the
// planned changes and prior state as totally separate, but later realized
// that it made sense for a plans.Plan to include the prior state directly
// so we can see what state the plan applies to. Hopefully later we'll
// clean this up some more so that we don't have two different ways to
// access the prior state (this and the ReadStateFile method).
ret, err := readTfplan(pr)
if err != nil {
return nil, err
}
prevRunStateFile, err := r.ReadPrevStateFile()
if err != nil {
return nil, fmt.Errorf("failed to read previous run state from plan file: %s", err)
}
priorStateFile, err := r.ReadStateFile()
if err != nil {
return nil, fmt.Errorf("failed to read prior state from plan file: %s", err)
}
ret.PrevRunState = prevRunStateFile.State
ret.PriorState = priorStateFile.State
return ret, nil
}
// ReadStateFile reads the state file embedded in the plan file, which
// represents the "PriorState" as defined in plans.Plan.
//
// If the plan file contains no embedded state file, the returned error is
// statefile.ErrNoState.
func (r *Reader) ReadStateFile() (*statefile.File, error) {
for _, file := range r.zip.File {
if file.Name == tfstateFilename {
r, err := file.Open()
if err != nil {
return nil, fmt.Errorf("failed to extract state from plan file: %s", err)
}
return statefile.Read(r)
}
}
return nil, statefile.ErrNoState
}
// ReadPrevStateFile reads the previous state file embedded in the plan file, which
// represents the "PrevRunState" as defined in plans.Plan.
//
// If the plan file contains no embedded previous state file, the returned error is
// statefile.ErrNoState.
func (r *Reader) ReadPrevStateFile() (*statefile.File, error) {
for _, file := range r.zip.File {
if file.Name == tfstatePreviousFilename {
r, err := file.Open()
if err != nil {
return nil, fmt.Errorf("failed to extract previous state from plan file: %s", err)
}
return statefile.Read(r)
}
}
return nil, statefile.ErrNoState
}
// ReadConfigSnapshot reads the configuration snapshot embedded in the plan
// file.
//
// This is a lower-level alternative to ReadConfig that just extracts the
// source files, without attempting to parse them.
func (r *Reader) ReadConfigSnapshot() (*configload.Snapshot, error) {
return readConfigSnapshot(&r.zip.Reader)
}
// ReadConfig reads the configuration embedded in the plan file.
//
// Internally this function delegates to the configs/configload package to
// parse the embedded configuration and so it returns diagnostics (rather than
// a native Go error as with other methods on Reader).
func (r *Reader) ReadConfig() (*configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
snap, err := r.ReadConfigSnapshot()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to read configuration from plan file",
fmt.Sprintf("The configuration file snapshot in the plan file could not be read: %s.", err),
))
return nil, diags
}
loader := configload.NewLoaderFromSnapshot(snap)
rootDir := snap.Modules[""].Dir // Root module base directory
config, configDiags := loader.LoadConfig(rootDir)
diags = diags.Append(configDiags)
return config, diags
}
// ReadDependencyLocks reads the dependency lock information embedded in
// the plan file.
//
// Some test codepaths create plan files without dependency lock information,
// but all main codepaths should populate this. If reading a file without
// the dependency information, this will return error diagnostics.
func (r *Reader) ReadDependencyLocks() (*depsfile.Locks, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
for _, file := range r.zip.File {
if file.Name == dependencyLocksFilename {
r, err := file.Open()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to read dependency locks from plan file",
fmt.Sprintf("Couldn't read the dependency lock information embedded in the plan file: %s.", err),
))
return nil, diags
}
src, err := ioutil.ReadAll(r)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to read dependency locks from plan file",
fmt.Sprintf("Couldn't read the dependency lock information embedded in the plan file: %s.", err),
))
return nil, diags
}
locks, moreDiags := depsfile.LoadLocksFromBytes(src, "<saved-plan>")
diags = diags.Append(moreDiags)
return locks, diags
}
}
// If we fall out here then this is a file without dependency information.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Saved plan has no dependency lock information",
"The specified saved plan file does not include any dependency lock information. This is a bug in the previous run of Terraform that created this file.",
))
return nil, diags
}
// Close closes the file, after which no other operations may be performed.
func (r *Reader) Close() error {
return r.zip.Close()
}