github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/planfile/config_snapshot.go (about) 1 package planfile 2 3 import ( 4 "archive/zip" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "path" 9 "sort" 10 "strings" 11 "time" 12 13 version "github.com/hashicorp/go-version" 14 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configload" 15 ) 16 17 const configSnapshotPrefix = "tfconfig/" 18 const configSnapshotManifestFile = configSnapshotPrefix + "modules.json" 19 const configSnapshotModulePrefix = configSnapshotPrefix + "m-" 20 21 type configSnapshotModuleRecord struct { 22 Key string `json:"Key"` 23 SourceAddr string `json:"Source,omitempty"` 24 VersionStr string `json:"Version,omitempty"` 25 Dir string `json:"Dir"` 26 } 27 type configSnapshotModuleManifest []configSnapshotModuleRecord 28 29 func readConfigSnapshot(z *zip.Reader) (*configload.Snapshot, error) { 30 // Errors from this function are expected to be reported with some 31 // additional prefix context about them being in a config snapshot, 32 // so they should not themselves refer to the config snapshot. 33 // They are also generally indicative of an invalid file, and so since 34 // plan files should not be hand-constructed we don't need to worry 35 // about making the messages user-actionable. 36 37 snap := &configload.Snapshot{ 38 Modules: map[string]*configload.SnapshotModule{}, 39 } 40 var manifestSrc []byte 41 42 // For processing our source files, we'll just sweep over all the files 43 // and react to the one-by-one to start, and then clean up afterwards 44 // when we'll presumably have found the manifest file. 45 for _, file := range z.File { 46 switch { 47 48 case file.Name == configSnapshotManifestFile: 49 // It's the manifest file, so we'll just read it raw into 50 // manifestSrc for now and process it below. 51 r, err := file.Open() 52 if err != nil { 53 return nil, fmt.Errorf("failed to open module manifest: %s", r) 54 } 55 manifestSrc, err = ioutil.ReadAll(r) 56 if err != nil { 57 return nil, fmt.Errorf("failed to read module manifest: %s", r) 58 } 59 60 case strings.HasPrefix(file.Name, configSnapshotModulePrefix): 61 relName := file.Name[len(configSnapshotModulePrefix):] 62 moduleKey, fileName := path.Split(relName) 63 64 // moduleKey should currently have a trailing slash on it, which we 65 // can use to recognize the difference between the root module 66 // (just a trailing slash) and no module path at all (empty string). 67 if moduleKey == "" { 68 // ignore invalid config entry 69 continue 70 } 71 moduleKey = moduleKey[:len(moduleKey)-1] // trim trailing slash 72 73 r, err := file.Open() 74 if err != nil { 75 return nil, fmt.Errorf("failed to open snapshot of %s from module %q: %s", fileName, moduleKey, err) 76 } 77 fileSrc, err := ioutil.ReadAll(r) 78 if err != nil { 79 return nil, fmt.Errorf("failed to read snapshot of %s from module %q: %s", fileName, moduleKey, err) 80 } 81 82 if _, exists := snap.Modules[moduleKey]; !exists { 83 snap.Modules[moduleKey] = &configload.SnapshotModule{ 84 Files: map[string][]byte{}, 85 // Will fill in everything else afterwards, when we 86 // process the manifest. 87 } 88 } 89 snap.Modules[moduleKey].Files[fileName] = fileSrc 90 } 91 } 92 93 if manifestSrc == nil { 94 return nil, fmt.Errorf("config snapshot does not have manifest file") 95 } 96 97 var manifest configSnapshotModuleManifest 98 err := json.Unmarshal(manifestSrc, &manifest) 99 if err != nil { 100 return nil, fmt.Errorf("invalid module manifest: %s", err) 101 } 102 103 for _, record := range manifest { 104 modSnap, exists := snap.Modules[record.Key] 105 if !exists { 106 // We'll allow this, assuming that it's a module with no files. 107 // This is still weird, since we generally reject modules with 108 // no files, but we'll allow it because downstream errors will 109 // catch it in that case. 110 modSnap = &configload.SnapshotModule{ 111 Files: map[string][]byte{}, 112 } 113 snap.Modules[record.Key] = modSnap 114 } 115 modSnap.SourceAddr = record.SourceAddr 116 modSnap.Dir = record.Dir 117 if record.VersionStr != "" { 118 v, err := version.NewVersion(record.VersionStr) 119 if err != nil { 120 return nil, fmt.Errorf("manifest has invalid version string %q for module %q", record.VersionStr, record.Key) 121 } 122 modSnap.Version = v 123 } 124 } 125 126 // Finally, we'll make sure we don't have any errant files for modules that 127 // aren't in the manifest. 128 for k := range snap.Modules { 129 found := false 130 for _, record := range manifest { 131 if record.Key == k { 132 found = true 133 break 134 } 135 } 136 if !found { 137 return nil, fmt.Errorf("found files for module %q that isn't recorded in the manifest", k) 138 } 139 } 140 141 return snap, nil 142 } 143 144 // writeConfigSnapshot adds to the given zip.Writer one or more files 145 // representing the given snapshot. 146 // 147 // This file creates new files in the writer, so any already-open writer 148 // for the file will be invalidated by this call. The writer remains open 149 // when this function returns. 150 func writeConfigSnapshot(snap *configload.Snapshot, z *zip.Writer) error { 151 // Errors from this function are expected to be reported with some 152 // additional prefix context about them being in a config snapshot, 153 // so they should not themselves refer to the config snapshot. 154 // They are also indicative of a bug in the caller, so they do not 155 // need to be user-actionable. 156 157 var manifest configSnapshotModuleManifest 158 keys := make([]string, 0, len(snap.Modules)) 159 for k := range snap.Modules { 160 keys = append(keys, k) 161 } 162 sort.Strings(keys) 163 164 // We'll re-use this fileheader for each Create we do below. 165 166 for _, k := range keys { 167 snapMod := snap.Modules[k] 168 record := configSnapshotModuleRecord{ 169 Dir: snapMod.Dir, 170 Key: k, 171 SourceAddr: snapMod.SourceAddr, 172 } 173 if snapMod.Version != nil { 174 record.VersionStr = snapMod.Version.String() 175 } 176 manifest = append(manifest, record) 177 178 pathPrefix := fmt.Sprintf("%s%s/", configSnapshotModulePrefix, k) 179 for filename, src := range snapMod.Files { 180 zh := &zip.FileHeader{ 181 Name: pathPrefix + filename, 182 Method: zip.Deflate, 183 Modified: time.Now(), 184 } 185 w, err := z.CreateHeader(zh) 186 if err != nil { 187 return fmt.Errorf("failed to create snapshot of %s from module %q: %s", zh.Name, k, err) 188 } 189 _, err = w.Write(src) 190 if err != nil { 191 return fmt.Errorf("failed to write snapshot of %s from module %q: %s", zh.Name, k, err) 192 } 193 } 194 } 195 196 // Now we'll write our manifest 197 { 198 zh := &zip.FileHeader{ 199 Name: configSnapshotManifestFile, 200 Method: zip.Deflate, 201 Modified: time.Now(), 202 } 203 src, err := json.MarshalIndent(manifest, "", " ") 204 if err != nil { 205 return fmt.Errorf("failed to serialize module manifest: %s", err) 206 } 207 w, err := z.CreateHeader(zh) 208 if err != nil { 209 return fmt.Errorf("failed to create module manifest: %s", err) 210 } 211 _, err = w.Write(src) 212 if err != nil { 213 return fmt.Errorf("failed to write module manifest: %s", err) 214 } 215 } 216 217 return nil 218 }