github.com/hashicorp/terraform-plugin-sdk@v1.17.2/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/hashicorp/terraform-plugin-sdk/internal/configs"
    10  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configload"
    11  	"github.com/hashicorp/terraform-plugin-sdk/internal/plans"
    12  	"github.com/hashicorp/terraform-plugin-sdk/internal/states/statefile"
    13  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
    14  )
    15  
    16  const tfstateFilename = "tfstate"
    17  
    18  // Reader is the main type used to read plan files. Create a Reader by calling
    19  // Open.
    20  //
    21  // A plan file is a random-access file format, so methods of Reader must
    22  // be used to access the individual portions of the file for further
    23  // processing.
    24  type Reader struct {
    25  	zip *zip.ReadCloser
    26  }
    27  
    28  // Open creates a Reader for the file at the given filename, or returns an
    29  // error if the file doesn't seem to be a planfile.
    30  func Open(filename string) (*Reader, error) {
    31  	r, err := zip.OpenReader(filename)
    32  	if err != nil {
    33  		// To give a better error message, we'll sniff to see if this looks
    34  		// like our old plan format from versions prior to 0.12.
    35  		if b, sErr := ioutil.ReadFile(filename); sErr == nil {
    36  			if bytes.HasPrefix(b, []byte("tfplan")) {
    37  				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")
    38  			}
    39  		}
    40  		return nil, err
    41  	}
    42  
    43  	// Sniff to make sure this looks like a plan file, as opposed to any other
    44  	// random zip file the user might have around.
    45  	var planFile *zip.File
    46  	for _, file := range r.File {
    47  		if file.Name == tfplanFilename {
    48  			planFile = file
    49  			break
    50  		}
    51  	}
    52  	if planFile == nil {
    53  		return nil, fmt.Errorf("the given file is not a valid plan file")
    54  	}
    55  
    56  	// For now, we'll just accept the presence of the tfplan file as enough,
    57  	// and wait to validate the version when the caller requests the plan
    58  	// itself.
    59  
    60  	return &Reader{
    61  		zip: r,
    62  	}, nil
    63  }
    64  
    65  // ReadPlan reads the plan embedded in the plan file.
    66  //
    67  // Errors can be returned for various reasons, including if the plan file
    68  // is not of an appropriate format version, if it was created by a different
    69  // version of Terraform, if it is invalid, etc.
    70  func (r *Reader) ReadPlan() (*plans.Plan, error) {
    71  	var planFile *zip.File
    72  	for _, file := range r.zip.File {
    73  		if file.Name == tfplanFilename {
    74  			planFile = file
    75  			break
    76  		}
    77  	}
    78  	if planFile == nil {
    79  		// This should never happen because we checked for this file during
    80  		// Open, but we'll check anyway to be safe.
    81  		return nil, fmt.Errorf("the plan file is invalid")
    82  	}
    83  
    84  	pr, err := planFile.Open()
    85  	if err != nil {
    86  		return nil, fmt.Errorf("failed to retrieve plan from plan file: %s", err)
    87  	}
    88  	defer pr.Close()
    89  
    90  	return readTfplan(pr)
    91  }
    92  
    93  // ReadStateFile reads the state file embedded in the plan file.
    94  //
    95  // If the plan file contains no embedded state file, the returned error is
    96  // statefile.ErrNoState.
    97  func (r *Reader) ReadStateFile() (*statefile.File, error) {
    98  	for _, file := range r.zip.File {
    99  		if file.Name == tfstateFilename {
   100  			r, err := file.Open()
   101  			if err != nil {
   102  				return nil, fmt.Errorf("failed to extract state from plan file: %s", err)
   103  			}
   104  			return statefile.Read(r)
   105  		}
   106  	}
   107  	return nil, statefile.ErrNoState
   108  }
   109  
   110  // ReadConfigSnapshot reads the configuration snapshot embedded in the plan
   111  // file.
   112  //
   113  // This is a lower-level alternative to ReadConfig that just extracts the
   114  // source files, without attempting to parse them.
   115  func (r *Reader) ReadConfigSnapshot() (*configload.Snapshot, error) {
   116  	return readConfigSnapshot(&r.zip.Reader)
   117  }
   118  
   119  // ReadConfig reads the configuration embedded in the plan file.
   120  //
   121  // Internally this function delegates to the configs/configload package to
   122  // parse the embedded configuration and so it returns diagnostics (rather than
   123  // a native Go error as with other methods on Reader).
   124  func (r *Reader) ReadConfig() (*configs.Config, tfdiags.Diagnostics) {
   125  	var diags tfdiags.Diagnostics
   126  
   127  	snap, err := r.ReadConfigSnapshot()
   128  	if err != nil {
   129  		diags = diags.Append(tfdiags.Sourceless(
   130  			tfdiags.Error,
   131  			"Failed to read configuration from plan file",
   132  			fmt.Sprintf("The configuration file snapshot in the plan file could not be read: %s.", err),
   133  		))
   134  		return nil, diags
   135  	}
   136  
   137  	loader := configload.NewLoaderFromSnapshot(snap)
   138  	rootDir := snap.Modules[""].Dir // Root module base directory
   139  	config, configDiags := loader.LoadConfig(rootDir)
   140  	diags = diags.Append(configDiags)
   141  
   142  	return config, diags
   143  }
   144  
   145  // Close closes the file, after which no other operations may be performed.
   146  func (r *Reader) Close() error {
   147  	return r.zip.Close()
   148  }