mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
2d373f16fa
Signed-off-by: guangwu <guoguangwug@gmail.com>
185 lines
5.3 KiB
Go
185 lines
5.3 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package modsdir
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
version "github.com/hashicorp/go-version"
|
|
|
|
"github.com/opentofu/opentofu/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 := io.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: %w", 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: %w", 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 "tofu 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 OpenTofu.
|
|
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
|
|
}
|
|
defer r.Close()
|
|
return ReadManifestSnapshot(r)
|
|
}
|
|
|
|
func (m Manifest) WriteSnapshot(w io.Writer) error {
|
|
var write manifestSnapshotFile
|
|
|
|
var keys []string
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
record := m[k]
|
|
|
|
// 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)
|
|
}
|