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