github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/plans/planfile/reader.go (about)

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