github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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/configs" 10 "github.com/hashicorp/terraform/configs/configload" 11 "github.com/hashicorp/terraform/plans" 12 "github.com/hashicorp/terraform/states/statefile" 13 "github.com/hashicorp/terraform/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 stateFile, err := statefile.Read(r) 105 if err == nil { 106 err = stateFile.CheckTerraformVersion() 107 } 108 return stateFile, err 109 } 110 } 111 return nil, statefile.ErrNoState 112 } 113 114 // ReadConfigSnapshot reads the configuration snapshot embedded in the plan 115 // file. 116 // 117 // This is a lower-level alternative to ReadConfig that just extracts the 118 // source files, without attempting to parse them. 119 func (r *Reader) ReadConfigSnapshot() (*configload.Snapshot, error) { 120 return readConfigSnapshot(&r.zip.Reader) 121 } 122 123 // ReadConfig reads the configuration embedded in the plan file. 124 // 125 // Internally this function delegates to the configs/configload package to 126 // parse the embedded configuration and so it returns diagnostics (rather than 127 // a native Go error as with other methods on Reader). 128 func (r *Reader) ReadConfig() (*configs.Config, tfdiags.Diagnostics) { 129 var diags tfdiags.Diagnostics 130 131 snap, err := r.ReadConfigSnapshot() 132 if err != nil { 133 diags = diags.Append(tfdiags.Sourceless( 134 tfdiags.Error, 135 "Failed to read configuration from plan file", 136 fmt.Sprintf("The configuration file snapshot in the plan file could not be read: %s.", err), 137 )) 138 return nil, diags 139 } 140 141 loader := configload.NewLoaderFromSnapshot(snap) 142 rootDir := snap.Modules[""].Dir // Root module base directory 143 config, configDiags := loader.LoadConfig(rootDir) 144 diags = diags.Append(configDiags) 145 146 return config, diags 147 } 148 149 // Close closes the file, after which no other operations may be performed. 150 func (r *Reader) Close() error { 151 return r.zip.Close() 152 }