github.com/opentofu/opentofu@v1.7.1/internal/plans/planfile/reader.go (about)

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