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