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 }