mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
31349a9c3a
This is part of a general effort to move all of Terraform's non-library package surface under internal in order to reinforce that these are for internal use within Terraform only. If you were previously importing packages under this prefix into an external codebase, you could pin to an earlier release tag as an interim solution until you've make a plan to achieve the same functionality some other way.
505 lines
14 KiB
Go
505 lines
14 KiB
Go
package configload
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"time"
|
|
|
|
version "github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/terraform/internal/configs"
|
|
"github.com/hashicorp/terraform/internal/modsdir"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
// LoadConfigWithSnapshot is a variant of LoadConfig that also simultaneously
|
|
// creates an in-memory snapshot of the configuration files used, which can
|
|
// be later used to create a loader that may read only from this snapshot.
|
|
func (l *Loader) LoadConfigWithSnapshot(rootDir string) (*configs.Config, *Snapshot, hcl.Diagnostics) {
|
|
rootMod, diags := l.parser.LoadConfigDir(rootDir)
|
|
if rootMod == nil {
|
|
return nil, nil, diags
|
|
}
|
|
|
|
snap := &Snapshot{
|
|
Modules: map[string]*SnapshotModule{},
|
|
}
|
|
walker := l.makeModuleWalkerSnapshot(snap)
|
|
cfg, cDiags := configs.BuildConfig(rootMod, walker)
|
|
diags = append(diags, cDiags...)
|
|
|
|
addDiags := l.addModuleToSnapshot(snap, "", rootDir, "", nil)
|
|
diags = append(diags, addDiags...)
|
|
|
|
return cfg, snap, diags
|
|
}
|
|
|
|
// NewLoaderFromSnapshot creates a Loader that reads files only from the
|
|
// given snapshot.
|
|
//
|
|
// A snapshot-based loader cannot install modules, so calling InstallModules
|
|
// on the return value will cause a panic.
|
|
//
|
|
// A snapshot-based loader also has access only to configuration files. Its
|
|
// underlying parser does not have access to other files in the native
|
|
// filesystem, such as values files. For those, either use a normal loader
|
|
// (created by NewLoader) or use the configs.Parser API directly.
|
|
func NewLoaderFromSnapshot(snap *Snapshot) *Loader {
|
|
fs := snapshotFS{snap}
|
|
parser := configs.NewParser(fs)
|
|
|
|
ret := &Loader{
|
|
parser: parser,
|
|
modules: moduleMgr{
|
|
FS: afero.Afero{Fs: fs},
|
|
CanInstall: false,
|
|
manifest: snap.moduleManifest(),
|
|
},
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// Snapshot is an in-memory representation of the source files from a
|
|
// configuration, which can be used as an alternative configurations source
|
|
// for a loader with NewLoaderFromSnapshot.
|
|
//
|
|
// The primary purpose of a Snapshot is to build the configuration portion
|
|
// of a plan file (see ../../plans/planfile) so that it can later be reloaded
|
|
// and used to recover the exact configuration that the plan was built from.
|
|
type Snapshot struct {
|
|
// Modules is a map from opaque module keys (suitable for use as directory
|
|
// names on all supported operating systems) to the snapshot information
|
|
// about each module.
|
|
Modules map[string]*SnapshotModule
|
|
}
|
|
|
|
// NewEmptySnapshot constructs and returns a snapshot containing only an empty
|
|
// root module. This is not useful for anything except placeholders in tests.
|
|
func NewEmptySnapshot() *Snapshot {
|
|
return &Snapshot{
|
|
Modules: map[string]*SnapshotModule{
|
|
"": &SnapshotModule{
|
|
Files: map[string][]byte{},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// SnapshotModule represents a single module within a Snapshot.
|
|
type SnapshotModule struct {
|
|
// Dir is the path, relative to the root directory given when the
|
|
// snapshot was created, where the module appears in the snapshot's
|
|
// virtual filesystem.
|
|
Dir string
|
|
|
|
// Files is a map from each configuration file filename for the
|
|
// module to a raw byte representation of the source file contents.
|
|
Files map[string][]byte
|
|
|
|
// SourceAddr is the source address given for this module in configuration.
|
|
SourceAddr string `json:"Source"`
|
|
|
|
// Version is the version of the module that is installed, or nil if
|
|
// the module is installed from a source that does not support versions.
|
|
Version *version.Version `json:"-"`
|
|
}
|
|
|
|
// moduleManifest constructs a module manifest based on the contents of
|
|
// the receiving snapshot.
|
|
func (s *Snapshot) moduleManifest() modsdir.Manifest {
|
|
ret := make(modsdir.Manifest)
|
|
|
|
for k, modSnap := range s.Modules {
|
|
ret[k] = modsdir.Record{
|
|
Key: k,
|
|
Dir: modSnap.Dir,
|
|
SourceAddr: modSnap.SourceAddr,
|
|
Version: modSnap.Version,
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// makeModuleWalkerSnapshot creates a configs.ModuleWalker that will exhibit
|
|
// the same lookup behaviors as l.moduleWalkerLoad but will additionally write
|
|
// source files from the referenced modules into the given snapshot.
|
|
func (l *Loader) makeModuleWalkerSnapshot(snap *Snapshot) configs.ModuleWalker {
|
|
return configs.ModuleWalkerFunc(
|
|
func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
|
|
mod, v, diags := l.moduleWalkerLoad(req)
|
|
if diags.HasErrors() {
|
|
return mod, v, diags
|
|
}
|
|
|
|
key := l.modules.manifest.ModuleKey(req.Path)
|
|
record, exists := l.modules.manifest[key]
|
|
|
|
if !exists {
|
|
// Should never happen, since otherwise moduleWalkerLoader would've
|
|
// returned an error and we would've returned already.
|
|
panic(fmt.Sprintf("module %s is not present in manifest", key))
|
|
}
|
|
|
|
addDiags := l.addModuleToSnapshot(snap, key, record.Dir, record.SourceAddr, record.Version)
|
|
diags = append(diags, addDiags...)
|
|
|
|
return mod, v, diags
|
|
},
|
|
)
|
|
}
|
|
|
|
func (l *Loader) addModuleToSnapshot(snap *Snapshot, key string, dir string, sourceAddr string, v *version.Version) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
|
|
primaryFiles, overrideFiles, moreDiags := l.parser.ConfigDirFiles(dir)
|
|
if moreDiags.HasErrors() {
|
|
// Any diagnostics we get here should be already present
|
|
// in diags, so it's weird if we get here but we'll allow it
|
|
// and return a general error message in that case.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Failed to read directory for module",
|
|
Detail: fmt.Sprintf("The source directory %s could not be read", dir),
|
|
})
|
|
return diags
|
|
}
|
|
|
|
snapMod := &SnapshotModule{
|
|
Dir: dir,
|
|
Files: map[string][]byte{},
|
|
SourceAddr: sourceAddr,
|
|
Version: v,
|
|
}
|
|
|
|
files := make([]string, 0, len(primaryFiles)+len(overrideFiles))
|
|
files = append(files, primaryFiles...)
|
|
files = append(files, overrideFiles...)
|
|
sources := l.Sources() // should be populated with all the files we need by now
|
|
for _, filePath := range files {
|
|
filename := filepath.Base(filePath)
|
|
src, exists := sources[filePath]
|
|
if !exists {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Missing source file for snapshot",
|
|
Detail: fmt.Sprintf("The source code for file %s could not be found to produce a configuration snapshot.", filePath),
|
|
})
|
|
continue
|
|
}
|
|
snapMod.Files[filepath.Clean(filename)] = src
|
|
}
|
|
|
|
snap.Modules[key] = snapMod
|
|
|
|
return diags
|
|
}
|
|
|
|
// snapshotFS is an implementation of afero.Fs that reads from a snapshot.
|
|
//
|
|
// This is not intended as a general-purpose filesystem implementation. Instead,
|
|
// it just supports the minimal functionality required to support the
|
|
// configuration loader and parser as an implementation detail of creating
|
|
// a loader from a snapshot.
|
|
type snapshotFS struct {
|
|
snap *Snapshot
|
|
}
|
|
|
|
var _ afero.Fs = snapshotFS{}
|
|
|
|
func (fs snapshotFS) Create(name string) (afero.File, error) {
|
|
return nil, fmt.Errorf("cannot create file inside configuration snapshot")
|
|
}
|
|
|
|
func (fs snapshotFS) Mkdir(name string, perm os.FileMode) error {
|
|
return fmt.Errorf("cannot create directory inside configuration snapshot")
|
|
}
|
|
|
|
func (fs snapshotFS) MkdirAll(name string, perm os.FileMode) error {
|
|
return fmt.Errorf("cannot create directories inside configuration snapshot")
|
|
}
|
|
|
|
func (fs snapshotFS) Open(name string) (afero.File, error) {
|
|
|
|
// Our "filesystem" is sparsely populated only with the directories
|
|
// mentioned by modules in our snapshot, so the high-level process
|
|
// for opening a file is:
|
|
// - Find the module snapshot corresponding to the containing directory
|
|
// - Find the file within that snapshot
|
|
// - Wrap the resulting byte slice in a snapshotFile to return
|
|
//
|
|
// The other possibility handled here is if the given name is for the
|
|
// module directory itself, in which case we'll return a snapshotDir
|
|
// instead.
|
|
//
|
|
// This function doesn't try to be incredibly robust in supporting
|
|
// different permutations of paths, etc because in practice we only
|
|
// need to support the path forms that our own loader and parser will
|
|
// generate.
|
|
|
|
dir := filepath.Dir(name)
|
|
fn := filepath.Base(name)
|
|
directDir := filepath.Clean(name)
|
|
|
|
// First we'll check to see if this is an exact path for a module directory.
|
|
// We need to do this first (rather than as part of the next loop below)
|
|
// because a module in a child directory of another module can otherwise
|
|
// appear to be a file in that parent directory.
|
|
for _, candidate := range fs.snap.Modules {
|
|
modDir := filepath.Clean(candidate.Dir)
|
|
if modDir == directDir {
|
|
// We've matched the module directory itself
|
|
filenames := make([]string, 0, len(candidate.Files))
|
|
for n := range candidate.Files {
|
|
filenames = append(filenames, n)
|
|
}
|
|
sort.Strings(filenames)
|
|
return snapshotDir{
|
|
filenames: filenames,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// If we get here then the given path isn't a module directory exactly, so
|
|
// we'll treat it as a file path and try to find a module directory it
|
|
// could be located in.
|
|
var modSnap *SnapshotModule
|
|
for _, candidate := range fs.snap.Modules {
|
|
modDir := filepath.Clean(candidate.Dir)
|
|
if modDir == dir {
|
|
modSnap = candidate
|
|
break
|
|
}
|
|
}
|
|
if modSnap == nil {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
|
|
src, exists := modSnap.Files[fn]
|
|
if !exists {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
|
|
return &snapshotFile{
|
|
src: src,
|
|
}, nil
|
|
}
|
|
|
|
func (fs snapshotFS) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
|
|
return fs.Open(name)
|
|
}
|
|
|
|
func (fs snapshotFS) Remove(name string) error {
|
|
return fmt.Errorf("cannot remove file inside configuration snapshot")
|
|
}
|
|
|
|
func (fs snapshotFS) RemoveAll(path string) error {
|
|
return fmt.Errorf("cannot remove files inside configuration snapshot")
|
|
}
|
|
|
|
func (fs snapshotFS) Rename(old, new string) error {
|
|
return fmt.Errorf("cannot rename file inside configuration snapshot")
|
|
}
|
|
|
|
func (fs snapshotFS) Stat(name string) (os.FileInfo, error) {
|
|
f, err := fs.Open(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, isDir := f.(snapshotDir)
|
|
return snapshotFileInfo{
|
|
name: filepath.Base(name),
|
|
isDir: isDir,
|
|
}, nil
|
|
}
|
|
|
|
func (fs snapshotFS) Name() string {
|
|
return "ConfigSnapshotFS"
|
|
}
|
|
|
|
func (fs snapshotFS) Chmod(name string, mode os.FileMode) error {
|
|
return fmt.Errorf("cannot set file mode inside configuration snapshot")
|
|
}
|
|
|
|
func (fs snapshotFS) Chtimes(name string, atime, mtime time.Time) error {
|
|
return fmt.Errorf("cannot set file times inside configuration snapshot")
|
|
}
|
|
|
|
type snapshotFile struct {
|
|
snapshotFileStub
|
|
src []byte
|
|
at int64
|
|
}
|
|
|
|
var _ afero.File = (*snapshotFile)(nil)
|
|
|
|
func (f *snapshotFile) Read(p []byte) (n int, err error) {
|
|
if len(p) > 0 && f.at == int64(len(f.src)) {
|
|
return 0, io.EOF
|
|
}
|
|
if f.at > int64(len(f.src)) {
|
|
return 0, io.ErrUnexpectedEOF
|
|
}
|
|
if int64(len(f.src))-f.at >= int64(len(p)) {
|
|
n = len(p)
|
|
} else {
|
|
n = int(int64(len(f.src)) - f.at)
|
|
}
|
|
copy(p, f.src[f.at:f.at+int64(n)])
|
|
f.at += int64(n)
|
|
return
|
|
}
|
|
|
|
func (f *snapshotFile) ReadAt(p []byte, off int64) (n int, err error) {
|
|
f.at = off
|
|
return f.Read(p)
|
|
}
|
|
|
|
func (f *snapshotFile) Seek(offset int64, whence int) (int64, error) {
|
|
switch whence {
|
|
case 0:
|
|
f.at = offset
|
|
case 1:
|
|
f.at += offset
|
|
case 2:
|
|
f.at = int64(len(f.src)) + offset
|
|
}
|
|
return f.at, nil
|
|
}
|
|
|
|
type snapshotDir struct {
|
|
snapshotFileStub
|
|
filenames []string
|
|
at int
|
|
}
|
|
|
|
var _ afero.File = snapshotDir{}
|
|
|
|
func (f snapshotDir) Readdir(count int) ([]os.FileInfo, error) {
|
|
names, err := f.Readdirnames(count)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret := make([]os.FileInfo, len(names))
|
|
for i, name := range names {
|
|
ret[i] = snapshotFileInfo{
|
|
name: name,
|
|
isDir: false,
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (f snapshotDir) Readdirnames(count int) ([]string, error) {
|
|
var outLen int
|
|
names := f.filenames[f.at:]
|
|
if count > 0 {
|
|
if len(names) < count {
|
|
outLen = len(names)
|
|
} else {
|
|
outLen = count
|
|
}
|
|
if len(names) == 0 {
|
|
return nil, io.EOF
|
|
}
|
|
} else {
|
|
outLen = len(names)
|
|
}
|
|
f.at += outLen
|
|
|
|
return names[:outLen], nil
|
|
}
|
|
|
|
// snapshotFileInfo is a minimal implementation of os.FileInfo to support our
|
|
// virtual filesystem from snapshots.
|
|
type snapshotFileInfo struct {
|
|
name string
|
|
isDir bool
|
|
}
|
|
|
|
var _ os.FileInfo = snapshotFileInfo{}
|
|
|
|
func (fi snapshotFileInfo) Name() string {
|
|
return fi.name
|
|
}
|
|
|
|
func (fi snapshotFileInfo) Size() int64 {
|
|
// In practice, our parser and loader never call Size
|
|
return -1
|
|
}
|
|
|
|
func (fi snapshotFileInfo) Mode() os.FileMode {
|
|
return os.ModePerm
|
|
}
|
|
|
|
func (fi snapshotFileInfo) ModTime() time.Time {
|
|
return time.Now()
|
|
}
|
|
|
|
func (fi snapshotFileInfo) IsDir() bool {
|
|
return fi.isDir
|
|
}
|
|
|
|
func (fi snapshotFileInfo) Sys() interface{} {
|
|
return nil
|
|
}
|
|
|
|
type snapshotFileStub struct{}
|
|
|
|
func (f snapshotFileStub) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (f snapshotFileStub) Read(p []byte) (n int, err error) {
|
|
return 0, fmt.Errorf("cannot read")
|
|
}
|
|
|
|
func (f snapshotFileStub) ReadAt(p []byte, off int64) (n int, err error) {
|
|
return 0, fmt.Errorf("cannot read")
|
|
}
|
|
|
|
func (f snapshotFileStub) Seek(offset int64, whence int) (int64, error) {
|
|
return 0, fmt.Errorf("cannot seek")
|
|
}
|
|
|
|
func (f snapshotFileStub) Write(p []byte) (n int, err error) {
|
|
return f.WriteAt(p, 0)
|
|
}
|
|
|
|
func (f snapshotFileStub) WriteAt(p []byte, off int64) (n int, err error) {
|
|
return 0, fmt.Errorf("cannot write to file in snapshot")
|
|
}
|
|
|
|
func (f snapshotFileStub) WriteString(s string) (n int, err error) {
|
|
return 0, fmt.Errorf("cannot write to file in snapshot")
|
|
}
|
|
|
|
func (f snapshotFileStub) Name() string {
|
|
// in practice, the loader and parser never use this
|
|
return "<unimplemented>"
|
|
}
|
|
|
|
func (f snapshotFileStub) Readdir(count int) ([]os.FileInfo, error) {
|
|
return nil, fmt.Errorf("cannot use Readdir on a file")
|
|
}
|
|
|
|
func (f snapshotFileStub) Readdirnames(count int) ([]string, error) {
|
|
return nil, fmt.Errorf("cannot use Readdir on a file")
|
|
}
|
|
|
|
func (f snapshotFileStub) Stat() (os.FileInfo, error) {
|
|
return nil, fmt.Errorf("cannot stat")
|
|
}
|
|
|
|
func (f snapshotFileStub) Sync() error {
|
|
return nil
|
|
}
|
|
|
|
func (f snapshotFileStub) Truncate(size int64) error {
|
|
return fmt.Errorf("cannot write to file in snapshot")
|
|
}
|