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  }