github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/plans/planfile/reader.go (about)

     1  package planfile
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"fmt"
     7  	"io/ioutil"
     8  
     9  	"github.com/iaas-resource-provision/iaas-rpc/internal/configs"
    10  	"github.com/iaas-resource-provision/iaas-rpc/internal/configs/configload"
    11  	"github.com/iaas-resource-provision/iaas-rpc/internal/plans"
    12  	"github.com/iaas-resource-provision/iaas-rpc/internal/states/statefile"
    13  	"github.com/iaas-resource-provision/iaas-rpc/internal/tfdiags"
    14  )
    15  
    16  const tfstateFilename = "tfstate"
    17  const 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.
    25  type 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.
    31  func 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.
    71  func (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.
   127  func (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.
   145  func (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.
   163  func (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).
   172  func (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.
   194  func (r *Reader) Close() error {
   195  	return r.zip.Close()
   196  }