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  }