terraform/configs/configload/module_manifest.go

133 lines
3.9 KiB
Go

package configload
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
version "github.com/hashicorp/go-version"
)
// moduleRecord represents some metadata about an installed module, as part
// of a moduleManifest.
type moduleRecord struct {
// Key is a unique identifier for this particular module, based on its
// position within the static module tree.
Key string `json:"Key"`
// SourceAddr is the source address given for this module in configuration.
// This is used only to detect if the source was changed in configuration
// since the module was last installed, which means that the installer
// must re-install it.
SourceAddr string `json:"Source"`
// Version is the exact version of the module, which results from parsing
// VersionStr. nil for un-versioned modules.
Version *version.Version `json:"-"`
// VersionStr is the version specifier string. This is used only for
// serialization in snapshots and should not be accessed or updated
// by any other codepaths; use "Version" instead.
VersionStr string `json:"Version,omitempty"`
// Dir is the path to the local directory where the module is installed.
Dir string `json:"Dir"`
}
// moduleManifest is a map used to keep track of the filesystem locations
// and other metadata about installed modules.
//
// The configuration loader refers to this, while the module installer updates
// it to reflect any changes to the installed modules.
type moduleManifest map[string]moduleRecord
func manifestKey(path []string) string {
return strings.Join(path, ".")
}
// manifestSnapshotFile is an internal struct used only to assist in our JSON
// serializtion of manifest snapshots. It should not be used for any other
// purposes.
type manifestSnapshotFile struct {
Records []moduleRecord `json:"Modules"`
}
const manifestFilename = "modules.json"
func (m *moduleMgr) manifestSnapshotPath() string {
return filepath.Join(m.Dir, manifestFilename)
}
// readModuleManifestSnapshot loads a manifest snapshot from the filesystem.
func (m *moduleMgr) readModuleManifestSnapshot() error {
src, err := m.FS.ReadFile(m.manifestSnapshotPath())
if err != nil {
if os.IsNotExist(err) {
// We'll treat a missing file as an empty manifest
m.manifest = make(moduleManifest)
return nil
}
return err
}
if len(src) == 0 {
// This should never happen, but we'll tolerate it as if it were
// a valid empty JSON object.
m.manifest = make(moduleManifest)
return nil
}
var read manifestSnapshotFile
err = json.Unmarshal(src, &read)
new := make(moduleManifest)
for _, record := range read.Records {
if record.VersionStr != "" {
record.Version, err = version.NewVersion(record.VersionStr)
if err != nil {
return fmt.Errorf("invalid version %q for %s: %s", record.VersionStr, record.Key, err)
}
}
if _, exists := new[record.Key]; exists {
// This should never happen in any valid file, so we'll catch it
// and report it to avoid confusing/undefined behavior if the
// snapshot file was edited incorrectly outside of Terraform.
return fmt.Errorf("snapshot file contains two records for path %s", record.Key)
}
new[record.Key] = record
}
m.manifest = new
return nil
}
// writeModuleManifestSnapshot writes a snapshot of the current manifest
// to the filesystem.
//
// The caller must guarantee no concurrent modifications of the manifest for
// the duration of a call to this function, or the behavior is undefined.
func (m *moduleMgr) writeModuleManifestSnapshot() error {
var write manifestSnapshotFile
for _, record := range m.manifest {
// Make sure VersionStr is in sync with Version, since we encourage
// callers to manipulate Version and ignore VersionStr.
if record.Version != nil {
record.VersionStr = record.Version.String()
} else {
record.VersionStr = ""
}
write.Records = append(write.Records, record)
}
src, err := json.Marshal(write)
if err != nil {
return err
}
return m.FS.WriteFile(m.manifestSnapshotPath(), src, os.ModePerm)
}