opentofu/internal/plans/planfile/reader.go
Martin Atkins 702413702c plans/planfile: Include dependency locks in saved plan files
We recently removed the legacy way we used to track the SHA256 hashes of
individual provider executables as part of a plans.Plan, because these
days we want to track the checksums of entire provider packages rather
than just the executable.

In order to achieve that new goal, we can save a copy of the dependency
lock information inside the plan file. This follows our existing precedent
of using exactly the same serialization formats we'd normally use for
this information, and thus we can reuse the existing models and
serializers and be confident we won't lose any detail in the round-trip.

As of this commit there's not yet anything actually making use of this
mechanism. In a subsequent commit we'll teach the main callers that write
and read plan files to include and expect (respectively) dependency
information, verifying that the available providers still match by the
time we're applying the plan.
2021-10-01 14:43:58 -07:00

243 lines
8.0 KiB
Go

package planfile
import (
"archive/zip"
"bytes"
"fmt"
"io/ioutil"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/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()
}