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