kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/configs/configload/loader_snapshot.go (about)

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