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