mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-18 12:42:58 -06:00
702413702c
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.
243 lines
8.0 KiB
Go
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()
|
|
}
|