package planfile import ( "archive/zip" "encoding/json" "fmt" "io/ioutil" "path" "sort" "strings" "time" version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/configs/configload" ) const configSnapshotPrefix = "tfconfig/" const configSnapshotManifestFile = configSnapshotPrefix + "modules.json" const configSnapshotModulePrefix = configSnapshotPrefix + "m-" type configSnapshotModuleRecord struct { Key string `json:"Key"` SourceAddr string `json:"Source,omitempty"` VersionStr string `json:"Version,omitempty"` Dir string `json:"Dir"` } type configSnapshotModuleManifest []configSnapshotModuleRecord func readConfigSnapshot(z *zip.Reader) (*configload.Snapshot, error) { // Errors from this function are expected to be reported with some // additional prefix context about them being in a config snapshot, // so they should not themselves refer to the config snapshot. // They are also generally indicative of an invalid file, and so since // plan files should not be hand-constructed we don't need to worry // about making the messages user-actionable. snap := &configload.Snapshot{ Modules: map[string]*configload.SnapshotModule{}, } var manifestSrc []byte // For processing our source files, we'll just sweep over all the files // and react to the one-by-one to start, and then clean up afterwards // when we'll presumably have found the manifest file. for _, file := range z.File { switch { case file.Name == configSnapshotManifestFile: // It's the manifest file, so we'll just read it raw into // manifestSrc for now and process it below. r, err := file.Open() if err != nil { return nil, fmt.Errorf("failed to open module manifest: %s", r) } manifestSrc, err = ioutil.ReadAll(r) if err != nil { return nil, fmt.Errorf("failed to read module manifest: %s", r) } case strings.HasPrefix(file.Name, configSnapshotModulePrefix): relName := file.Name[len(configSnapshotModulePrefix):] moduleKey, fileName := path.Split(relName) // moduleKey should currently have a trailing slash on it, which we // can use to recognize the difference between the root module // (just a trailing slash) and no module path at all (empty string). if moduleKey == "" { // ignore invalid config entry continue } moduleKey = moduleKey[:len(moduleKey)-1] // trim trailing slash r, err := file.Open() if err != nil { return nil, fmt.Errorf("failed to open snapshot of %s from module %q: %s", fileName, moduleKey, err) } fileSrc, err := ioutil.ReadAll(r) if err != nil { return nil, fmt.Errorf("failed to read snapshot of %s from module %q: %s", fileName, moduleKey, err) } if _, exists := snap.Modules[moduleKey]; !exists { snap.Modules[moduleKey] = &configload.SnapshotModule{ Files: map[string][]byte{}, // Will fill in everything else afterwards, when we // process the manifest. } } snap.Modules[moduleKey].Files[fileName] = fileSrc } } if manifestSrc == nil { return nil, fmt.Errorf("config snapshot does not have manifest file") } var manifest configSnapshotModuleManifest err := json.Unmarshal(manifestSrc, &manifest) if err != nil { return nil, fmt.Errorf("invalid module manifest: %s", err) } for _, record := range manifest { modSnap, exists := snap.Modules[record.Key] if !exists { // We'll allow this, assuming that it's a module with no files. // This is still weird, since we generally reject modules with // no files, but we'll allow it because downstream errors will // catch it in that case. modSnap = &configload.SnapshotModule{ Files: map[string][]byte{}, } snap.Modules[record.Key] = modSnap } modSnap.SourceAddr = record.SourceAddr modSnap.Dir = record.Dir if record.VersionStr != "" { v, err := version.NewVersion(record.VersionStr) if err != nil { return nil, fmt.Errorf("manifest has invalid version string %q for module %q", record.VersionStr, record.Key) } modSnap.Version = v } } // Finally, we'll make sure we don't have any errant files for modules that // aren't in the manifest. for k := range snap.Modules { found := false for _, record := range manifest { if record.Key == k { found = true break } } if !found { return nil, fmt.Errorf("found files for module %q that isn't recorded in the manifest", k) } } return snap, nil } // writeConfigSnapshot adds to the given zip.Writer one or more files // representing the given snapshot. // // This file creates new files in the writer, so any already-open writer // for the file will be invalidated by this call. The writer remains open // when this function returns. func writeConfigSnapshot(snap *configload.Snapshot, z *zip.Writer) error { // Errors from this function are expected to be reported with some // additional prefix context about them being in a config snapshot, // so they should not themselves refer to the config snapshot. // They are also indicative of a bug in the caller, so they do not // need to be user-actionable. var manifest configSnapshotModuleManifest keys := make([]string, 0, len(snap.Modules)) for k := range snap.Modules { keys = append(keys, k) } sort.Strings(keys) // We'll re-use this fileheader for each Create we do below. for _, k := range keys { snapMod := snap.Modules[k] record := configSnapshotModuleRecord{ Dir: snapMod.Dir, Key: k, SourceAddr: snapMod.SourceAddr, } if snapMod.Version != nil { record.VersionStr = snapMod.Version.String() } manifest = append(manifest, record) pathPrefix := fmt.Sprintf("%s%s/", configSnapshotModulePrefix, k) for filename, src := range snapMod.Files { zh := &zip.FileHeader{ Name: pathPrefix + filename, Method: zip.Deflate, Modified: time.Now(), } w, err := z.CreateHeader(zh) if err != nil { return fmt.Errorf("failed to create snapshot of %s from module %q: %s", zh.Name, k, err) } _, err = w.Write(src) if err != nil { return fmt.Errorf("failed to write snapshot of %s from module %q: %s", zh.Name, k, err) } } } // Now we'll write our manifest { zh := &zip.FileHeader{ Name: configSnapshotManifestFile, Method: zip.Deflate, Modified: time.Now(), } src, err := json.MarshalIndent(manifest, "", " ") if err != nil { return fmt.Errorf("failed to serialize module manifest: %s", err) } w, err := z.CreateHeader(zh) if err != nil { return fmt.Errorf("failed to create module manifest: %s", err) } _, err = w.Write(src) if err != nil { return fmt.Errorf("failed to write module manifest: %s", err) } } return nil }