opentofu/internal/modsdir/manifest.go
guangwu 2d373f16fa
fix: close manifest file (#1479)
Signed-off-by: guangwu <guoguangwug@gmail.com>
2024-04-10 14:41:53 +03:00

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)
}