github.com/opentofu/opentofu@v1.7.1/internal/configs/configload/loader_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 configload
     7  
     8  import (
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"sort"
    14  	"time"
    15  
    16  	version "github.com/hashicorp/go-version"
    17  	"github.com/hashicorp/hcl/v2"
    18  	"github.com/opentofu/opentofu/internal/configs"
    19  	"github.com/opentofu/opentofu/internal/modsdir"
    20  	"github.com/spf13/afero"
    21  )
    22  
    23  // LoadConfigWithSnapshot is a variant of LoadConfig that also simultaneously
    24  // creates an in-memory snapshot of the configuration files used, which can
    25  // be later used to create a loader that may read only from this snapshot.
    26  func (l *Loader) LoadConfigWithSnapshot(rootDir string) (*configs.Config, *Snapshot, hcl.Diagnostics) {
    27  	rootMod, diags := l.parser.LoadConfigDir(rootDir)
    28  	if rootMod == nil {
    29  		return nil, nil, diags
    30  	}
    31  
    32  	snap := &Snapshot{
    33  		Modules: map[string]*SnapshotModule{},
    34  	}
    35  	walker := l.makeModuleWalkerSnapshot(snap)
    36  	cfg, cDiags := configs.BuildConfig(rootMod, walker)
    37  	diags = append(diags, cDiags...)
    38  
    39  	addDiags := l.addModuleToSnapshot(snap, "", rootDir, "", nil)
    40  	diags = append(diags, addDiags...)
    41  
    42  	return cfg, snap, diags
    43  }
    44  
    45  // NewLoaderFromSnapshot creates a Loader that reads files only from the
    46  // given snapshot.
    47  //
    48  // A snapshot-based loader cannot install modules, so calling InstallModules
    49  // on the return value will cause a panic.
    50  //
    51  // A snapshot-based loader also has access only to configuration files. Its
    52  // underlying parser does not have access to other files in the native
    53  // filesystem, such as values files. For those, either use a normal loader
    54  // (created by NewLoader) or use the configs.Parser API directly.
    55  func NewLoaderFromSnapshot(snap *Snapshot) *Loader {
    56  	fs := snapshotFS{snap}
    57  	parser := configs.NewParser(fs)
    58  
    59  	ret := &Loader{
    60  		parser: parser,
    61  		modules: moduleMgr{
    62  			FS:         afero.Afero{Fs: fs},
    63  			CanInstall: false,
    64  			manifest:   snap.moduleManifest(),
    65  		},
    66  	}
    67  
    68  	return ret
    69  }
    70  
    71  // Snapshot is an in-memory representation of the source files from a
    72  // configuration, which can be used as an alternative configurations source
    73  // for a loader with NewLoaderFromSnapshot.
    74  //
    75  // The primary purpose of a Snapshot is to build the configuration portion
    76  // of a plan file (see ../../plans/planfile) so that it can later be reloaded
    77  // and used to recover the exact configuration that the plan was built from.
    78  type Snapshot struct {
    79  	// Modules is a map from opaque module keys (suitable for use as directory
    80  	// names on all supported operating systems) to the snapshot information
    81  	// about each module.
    82  	Modules map[string]*SnapshotModule
    83  }
    84  
    85  // NewEmptySnapshot constructs and returns a snapshot containing only an empty
    86  // root module. This is not useful for anything except placeholders in tests.
    87  func NewEmptySnapshot() *Snapshot {
    88  	return &Snapshot{
    89  		Modules: map[string]*SnapshotModule{
    90  			"": &SnapshotModule{
    91  				Files: map[string][]byte{},
    92  			},
    93  		},
    94  	}
    95  }
    96  
    97  // SnapshotModule represents a single module within a Snapshot.
    98  type SnapshotModule struct {
    99  	// Dir is the path, relative to the root directory given when the
   100  	// snapshot was created, where the module appears in the snapshot's
   101  	// virtual filesystem.
   102  	Dir string
   103  
   104  	// Files is a map from each configuration file filename for the
   105  	// module to a raw byte representation of the source file contents.
   106  	Files map[string][]byte
   107  
   108  	// SourceAddr is the source address given for this module in configuration.
   109  	SourceAddr string `json:"Source"`
   110  
   111  	// Version is the version of the module that is installed, or nil if
   112  	// the module is installed from a source that does not support versions.
   113  	Version *version.Version `json:"-"`
   114  }
   115  
   116  // moduleManifest constructs a module manifest based on the contents of
   117  // the receiving snapshot.
   118  func (s *Snapshot) moduleManifest() modsdir.Manifest {
   119  	ret := make(modsdir.Manifest)
   120  
   121  	for k, modSnap := range s.Modules {
   122  		ret[k] = modsdir.Record{
   123  			Key:        k,
   124  			Dir:        modSnap.Dir,
   125  			SourceAddr: modSnap.SourceAddr,
   126  			Version:    modSnap.Version,
   127  		}
   128  	}
   129  
   130  	return ret
   131  }
   132  
   133  // makeModuleWalkerSnapshot creates a configs.ModuleWalker that will exhibit
   134  // the same lookup behaviors as l.moduleWalkerLoad but will additionally write
   135  // source files from the referenced modules into the given snapshot.
   136  func (l *Loader) makeModuleWalkerSnapshot(snap *Snapshot) configs.ModuleWalker {
   137  	return configs.ModuleWalkerFunc(
   138  		func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   139  			mod, v, diags := l.moduleWalkerLoad(req)
   140  			if diags.HasErrors() {
   141  				return mod, v, diags
   142  			}
   143  
   144  			key := l.modules.manifest.ModuleKey(req.Path)
   145  			record, exists := l.modules.manifest[key]
   146  
   147  			if !exists {
   148  				// Should never happen, since otherwise moduleWalkerLoader would've
   149  				// returned an error and we would've returned already.
   150  				panic(fmt.Sprintf("module %s is not present in manifest", key))
   151  			}
   152  
   153  			addDiags := l.addModuleToSnapshot(snap, key, record.Dir, record.SourceAddr, record.Version)
   154  			diags = append(diags, addDiags...)
   155  
   156  			return mod, v, diags
   157  		},
   158  	)
   159  }
   160  
   161  func (l *Loader) addModuleToSnapshot(snap *Snapshot, key string, dir string, sourceAddr string, v *version.Version) hcl.Diagnostics {
   162  	var diags hcl.Diagnostics
   163  
   164  	primaryFiles, overrideFiles, moreDiags := l.parser.ConfigDirFiles(dir)
   165  	if moreDiags.HasErrors() {
   166  		// Any diagnostics we get here should be already present
   167  		// in diags, so it's weird if we get here but we'll allow it
   168  		// and return a general error message in that case.
   169  		diags = append(diags, &hcl.Diagnostic{
   170  			Severity: hcl.DiagError,
   171  			Summary:  "Failed to read directory for module",
   172  			Detail:   fmt.Sprintf("The source directory %s could not be read", dir),
   173  		})
   174  		return diags
   175  	}
   176  
   177  	snapMod := &SnapshotModule{
   178  		Dir:        dir,
   179  		Files:      map[string][]byte{},
   180  		SourceAddr: sourceAddr,
   181  		Version:    v,
   182  	}
   183  
   184  	files := make([]string, 0, len(primaryFiles)+len(overrideFiles))
   185  	files = append(files, primaryFiles...)
   186  	files = append(files, overrideFiles...)
   187  	sources := l.Sources() // should be populated with all the files we need by now
   188  	for _, filePath := range files {
   189  		filename := filepath.Base(filePath)
   190  		src, exists := sources[filePath]
   191  		if !exists {
   192  			diags = append(diags, &hcl.Diagnostic{
   193  				Severity: hcl.DiagError,
   194  				Summary:  "Missing source file for snapshot",
   195  				Detail:   fmt.Sprintf("The source code for file %s could not be found to produce a configuration snapshot.", filePath),
   196  			})
   197  			continue
   198  		}
   199  		snapMod.Files[filepath.Clean(filename)] = src
   200  	}
   201  
   202  	snap.Modules[key] = snapMod
   203  
   204  	return diags
   205  }
   206  
   207  // snapshotFS is an implementation of afero.Fs that reads from a snapshot.
   208  //
   209  // This is not intended as a general-purpose filesystem implementation. Instead,
   210  // it just supports the minimal functionality required to support the
   211  // configuration loader and parser as an implementation detail of creating
   212  // a loader from a snapshot.
   213  type snapshotFS struct {
   214  	snap *Snapshot
   215  }
   216  
   217  var _ afero.Fs = snapshotFS{}
   218  
   219  func (fs snapshotFS) Create(name string) (afero.File, error) {
   220  	return nil, fmt.Errorf("cannot create file inside configuration snapshot")
   221  }
   222  
   223  func (fs snapshotFS) Mkdir(name string, perm os.FileMode) error {
   224  	return fmt.Errorf("cannot create directory inside configuration snapshot")
   225  }
   226  
   227  func (fs snapshotFS) MkdirAll(name string, perm os.FileMode) error {
   228  	return fmt.Errorf("cannot create directories inside configuration snapshot")
   229  }
   230  
   231  func (fs snapshotFS) Open(name string) (afero.File, error) {
   232  
   233  	// Our "filesystem" is sparsely populated only with the directories
   234  	// mentioned by modules in our snapshot, so the high-level process
   235  	// for opening a file is:
   236  	// - Find the module snapshot corresponding to the containing directory
   237  	// - Find the file within that snapshot
   238  	// - Wrap the resulting byte slice in a snapshotFile to return
   239  	//
   240  	// The other possibility handled here is if the given name is for the
   241  	// module directory itself, in which case we'll return a snapshotDir
   242  	// instead.
   243  	//
   244  	// This function doesn't try to be incredibly robust in supporting
   245  	// different permutations of paths, etc because in practice we only
   246  	// need to support the path forms that our own loader and parser will
   247  	// generate.
   248  
   249  	dir := filepath.Dir(name)
   250  	fn := filepath.Base(name)
   251  	directDir := filepath.Clean(name)
   252  
   253  	// First we'll check to see if this is an exact path for a module directory.
   254  	// We need to do this first (rather than as part of the next loop below)
   255  	// because a module in a child directory of another module can otherwise
   256  	// appear to be a file in that parent directory.
   257  	for _, candidate := range fs.snap.Modules {
   258  		modDir := filepath.Clean(candidate.Dir)
   259  		if modDir == directDir {
   260  			// We've matched the module directory itself
   261  			filenames := make([]string, 0, len(candidate.Files))
   262  			for n := range candidate.Files {
   263  				filenames = append(filenames, n)
   264  			}
   265  			sort.Strings(filenames)
   266  			return &snapshotDir{
   267  				filenames: filenames,
   268  			}, nil
   269  		}
   270  	}
   271  
   272  	// If we get here then the given path isn't a module directory exactly, so
   273  	// we'll treat it as a file path and try to find a module directory it
   274  	// could be located in.
   275  	var modSnap *SnapshotModule
   276  	for _, candidate := range fs.snap.Modules {
   277  		modDir := filepath.Clean(candidate.Dir)
   278  		if modDir == dir {
   279  			modSnap = candidate
   280  			break
   281  		}
   282  	}
   283  	if modSnap == nil {
   284  		return nil, os.ErrNotExist
   285  	}
   286  
   287  	src, exists := modSnap.Files[fn]
   288  	if !exists {
   289  		return nil, os.ErrNotExist
   290  	}
   291  
   292  	return &snapshotFile{
   293  		src: src,
   294  	}, nil
   295  }
   296  
   297  func (fs snapshotFS) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
   298  	return fs.Open(name)
   299  }
   300  
   301  func (fs snapshotFS) Remove(name string) error {
   302  	return fmt.Errorf("cannot remove file inside configuration snapshot")
   303  }
   304  
   305  func (fs snapshotFS) RemoveAll(path string) error {
   306  	return fmt.Errorf("cannot remove files inside configuration snapshot")
   307  }
   308  
   309  func (fs snapshotFS) Rename(old, new string) error {
   310  	return fmt.Errorf("cannot rename file inside configuration snapshot")
   311  }
   312  
   313  func (fs snapshotFS) Stat(name string) (os.FileInfo, error) {
   314  	f, err := fs.Open(name)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	_, isDir := f.(*snapshotDir)
   319  	return snapshotFileInfo{
   320  		name:  filepath.Base(name),
   321  		isDir: isDir,
   322  	}, nil
   323  }
   324  
   325  func (fs snapshotFS) Name() string {
   326  	return "ConfigSnapshotFS"
   327  }
   328  
   329  func (fs snapshotFS) Chmod(name string, mode os.FileMode) error {
   330  	return fmt.Errorf("cannot set file mode inside configuration snapshot")
   331  }
   332  
   333  func (snapshotFS) Chown(name string, uid int, gid int) error {
   334  	return fmt.Errorf("cannot set file owner inside configuration snapshot")
   335  }
   336  
   337  func (fs snapshotFS) Chtimes(name string, atime, mtime time.Time) error {
   338  	return fmt.Errorf("cannot set file times inside configuration snapshot")
   339  }
   340  
   341  type snapshotFile struct {
   342  	snapshotFileStub
   343  	src []byte
   344  	at  int64
   345  }
   346  
   347  var _ afero.File = (*snapshotFile)(nil)
   348  
   349  func (f *snapshotFile) Read(p []byte) (n int, err error) {
   350  	if len(p) > 0 && f.at == int64(len(f.src)) {
   351  		return 0, io.EOF
   352  	}
   353  	if f.at > int64(len(f.src)) {
   354  		return 0, io.ErrUnexpectedEOF
   355  	}
   356  	if int64(len(f.src))-f.at >= int64(len(p)) {
   357  		n = len(p)
   358  	} else {
   359  		n = int(int64(len(f.src)) - f.at)
   360  	}
   361  	copy(p, f.src[f.at:f.at+int64(n)])
   362  	f.at += int64(n)
   363  	return
   364  }
   365  
   366  func (f *snapshotFile) ReadAt(p []byte, off int64) (n int, err error) {
   367  	f.at = off
   368  	return f.Read(p)
   369  }
   370  
   371  func (f *snapshotFile) Seek(offset int64, whence int) (int64, error) {
   372  	switch whence {
   373  	case 0:
   374  		f.at = offset
   375  	case 1:
   376  		f.at += offset
   377  	case 2:
   378  		f.at = int64(len(f.src)) + offset
   379  	}
   380  	return f.at, nil
   381  }
   382  
   383  type snapshotDir struct {
   384  	snapshotFileStub
   385  	filenames []string
   386  	at        int
   387  }
   388  
   389  var _ afero.File = (*snapshotDir)(nil)
   390  
   391  func (f *snapshotDir) Readdir(count int) ([]os.FileInfo, error) {
   392  	names, err := f.Readdirnames(count)
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  	ret := make([]os.FileInfo, len(names))
   397  	for i, name := range names {
   398  		ret[i] = snapshotFileInfo{
   399  			name:  name,
   400  			isDir: false,
   401  		}
   402  	}
   403  	return ret, nil
   404  }
   405  
   406  func (f *snapshotDir) Readdirnames(count int) ([]string, error) {
   407  	var outLen int
   408  	names := f.filenames[f.at:]
   409  	if count > 0 {
   410  		if len(names) < count {
   411  			outLen = len(names)
   412  		} else {
   413  			outLen = count
   414  		}
   415  		if len(names) == 0 {
   416  			return nil, io.EOF
   417  		}
   418  	} else {
   419  		outLen = len(names)
   420  	}
   421  	f.at += outLen
   422  
   423  	return names[:outLen], nil
   424  }
   425  
   426  // snapshotFileInfo is a minimal implementation of os.FileInfo to support our
   427  // virtual filesystem from snapshots.
   428  type snapshotFileInfo struct {
   429  	name  string
   430  	isDir bool
   431  }
   432  
   433  var _ os.FileInfo = snapshotFileInfo{}
   434  
   435  func (fi snapshotFileInfo) Name() string {
   436  	return fi.name
   437  }
   438  
   439  func (fi snapshotFileInfo) Size() int64 {
   440  	// In practice, our parser and loader never call Size
   441  	return -1
   442  }
   443  
   444  func (fi snapshotFileInfo) Mode() os.FileMode {
   445  	return os.ModePerm
   446  }
   447  
   448  func (fi snapshotFileInfo) ModTime() time.Time {
   449  	return time.Now()
   450  }
   451  
   452  func (fi snapshotFileInfo) IsDir() bool {
   453  	return fi.isDir
   454  }
   455  
   456  func (fi snapshotFileInfo) Sys() interface{} {
   457  	return nil
   458  }
   459  
   460  type snapshotFileStub struct{}
   461  
   462  func (f snapshotFileStub) Close() error {
   463  	return nil
   464  }
   465  
   466  func (f snapshotFileStub) Read(p []byte) (n int, err error) {
   467  	return 0, fmt.Errorf("cannot read")
   468  }
   469  
   470  func (f snapshotFileStub) ReadAt(p []byte, off int64) (n int, err error) {
   471  	return 0, fmt.Errorf("cannot read")
   472  }
   473  
   474  func (f snapshotFileStub) Seek(offset int64, whence int) (int64, error) {
   475  	return 0, fmt.Errorf("cannot seek")
   476  }
   477  
   478  func (f snapshotFileStub) Write(p []byte) (n int, err error) {
   479  	return f.WriteAt(p, 0)
   480  }
   481  
   482  func (f snapshotFileStub) WriteAt(p []byte, off int64) (n int, err error) {
   483  	return 0, fmt.Errorf("cannot write to file in snapshot")
   484  }
   485  
   486  func (f snapshotFileStub) WriteString(s string) (n int, err error) {
   487  	return 0, fmt.Errorf("cannot write to file in snapshot")
   488  }
   489  
   490  func (f snapshotFileStub) Name() string {
   491  	// in practice, the loader and parser never use this
   492  	return "<unimplemented>"
   493  }
   494  
   495  func (f snapshotFileStub) Readdir(count int) ([]os.FileInfo, error) {
   496  	return nil, fmt.Errorf("cannot use Readdir on a file")
   497  }
   498  
   499  func (f snapshotFileStub) Readdirnames(count int) ([]string, error) {
   500  	return nil, fmt.Errorf("cannot use Readdir on a file")
   501  }
   502  
   503  func (f snapshotFileStub) Stat() (os.FileInfo, error) {
   504  	return nil, fmt.Errorf("cannot stat")
   505  }
   506  
   507  func (f snapshotFileStub) Sync() error {
   508  	return nil
   509  }
   510  
   511  func (f snapshotFileStub) Truncate(size int64) error {
   512  	return fmt.Errorf("cannot write to file in snapshot")
   513  }