2018-06-20 01:30:59 +02:00
|
|
|
package planfile
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"path"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
version "github.com/hashicorp/go-version"
|
2021-05-17 21:17:09 +02:00
|
|
|
"github.com/hashicorp/terraform/internal/configs/configload"
|
2018-06-20 01:30:59 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|