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 }