mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
1a8da65314
It's been a long while since we gave close attention to the codepaths for module source address parsing and external module package installation. Due to their age, these codepaths often diverged from our modern practices such as representing address types in the addrs package, and encapsulating package installation details only in a particular location. In particular, this refactor makes source address parsing a separate step from module installation, which therefore makes the result of that parsing available to other Terraform subsystems which work with the configuration representation objects. This also presented the opportunity to better encapsulate our use of go-getter into a new package "getmodules" (echoing "getproviders"), which is intended to be the only part of Terraform that directly interacts with go-getter. This is largely just a refactor of the existing functionality into a new code organization, but there is one notable change in behavior here: the source address parsing now happens during configuration loading rather than module installation, which may cause errors about invalid addresses to be returned in different situations than before. That counts as backward compatible because we only promise to remain compatible with configurations that are _valid_, which means that they can be initialized, planned, and applied without any errors. This doesn't introduce any new error cases, and instead just makes a pre-existing error case be detected earlier. Our module registry client is still using its own special module address type from registry/regsrc for now, with a small shim from the new addrs.ModuleSourceRegistry type. Hopefully in a later commit we'll also rework the registry client to work with the new address type, but this commit is already big enough as it is.
172 lines
5.1 KiB
Go
172 lines
5.1 KiB
Go
package modsdir
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
version "github.com/hashicorp/go-version"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
)
|
|
|
|
// Record represents some metadata about an installed module, as part
|
|
// of a ModuleManifest.
|
|
type Record 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.
|
|
//
|
|
// This should always be the result of calling method String on an
|
|
// addrs.ModuleSource value, to get a suitably-normalized result.
|
|
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"`
|
|
}
|
|
|
|
// Manifest 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 Manifest map[string]Record
|
|
|
|
func (m Manifest) ModuleKey(path addrs.Module) string {
|
|
if len(path) == 0 {
|
|
return ""
|
|
}
|
|
return strings.Join([]string(path), ".")
|
|
|
|
}
|
|
|
|
// manifestSnapshotFile is an internal struct used only to assist in our JSON
|
|
// serialization of manifest snapshots. It should not be used for any other
|
|
// purpose.
|
|
type manifestSnapshotFile struct {
|
|
Records []Record `json:"Modules"`
|
|
}
|
|
|
|
func ReadManifestSnapshot(r io.Reader) (Manifest, error) {
|
|
src, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(src) == 0 {
|
|
// This should never happen, but we'll tolerate it as if it were
|
|
// a valid empty JSON object.
|
|
return make(Manifest), nil
|
|
}
|
|
|
|
var read manifestSnapshotFile
|
|
err = json.Unmarshal(src, &read)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error unmarshalling snapshot: %v", err)
|
|
}
|
|
new := make(Manifest)
|
|
for _, record := range read.Records {
|
|
if record.VersionStr != "" {
|
|
record.Version, err = version.NewVersion(record.VersionStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid version %q for %s: %s", record.VersionStr, record.Key, err)
|
|
}
|
|
}
|
|
|
|
// Historically we didn't normalize the module source addresses when
|
|
// writing them into the manifest, and so we'll make a best effort
|
|
// to normalize them back in on read so that we can just gracefully
|
|
// upgrade on the next "terraform init".
|
|
if record.SourceAddr != "" {
|
|
if addr, err := addrs.ParseModuleSource(record.SourceAddr); err == nil {
|
|
// This is a best effort sort of thing. If the source
|
|
// address isn't valid then we'll just leave it as-is
|
|
// and let another component detect that downstream,
|
|
// to preserve the old behavior in that case.
|
|
record.SourceAddr = addr.String()
|
|
}
|
|
}
|
|
|
|
// Ensure Windows is using the proper modules path format after
|
|
// reading the modules manifest Dir records
|
|
record.Dir = filepath.FromSlash(record.Dir)
|
|
|
|
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 nil, fmt.Errorf("snapshot file contains two records for path %s", record.Key)
|
|
}
|
|
new[record.Key] = record
|
|
}
|
|
return new, nil
|
|
}
|
|
|
|
func ReadManifestSnapshotForDir(dir string) (Manifest, error) {
|
|
fn := filepath.Join(dir, ManifestSnapshotFilename)
|
|
r, err := os.Open(fn)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return make(Manifest), nil // missing file is okay and treated as empty
|
|
}
|
|
return nil, err
|
|
}
|
|
return ReadManifestSnapshot(r)
|
|
}
|
|
|
|
func (m Manifest) WriteSnapshot(w io.Writer) error {
|
|
var write manifestSnapshotFile
|
|
|
|
for _, record := range m {
|
|
// 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 = ""
|
|
}
|
|
|
|
// Ensure Dir is written in a format that can be read by Linux and
|
|
// Windows nodes for remote and apply compatibility
|
|
record.Dir = filepath.ToSlash(record.Dir)
|
|
write.Records = append(write.Records, record)
|
|
}
|
|
|
|
src, err := json.Marshal(write)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = w.Write(src)
|
|
return err
|
|
}
|
|
|
|
func (m Manifest) WriteSnapshotToDir(dir string) error {
|
|
fn := filepath.Join(dir, ManifestSnapshotFilename)
|
|
log.Printf("[TRACE] modsdir: writing modules manifest to %s", fn)
|
|
w, err := os.Create(fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return m.WriteSnapshot(w)
|
|
}
|