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  }