Files
docker-cli/cli/context/store/metadatastore.go
Sebastiaan van Stijn 7b624841c4 update minimum go version to go1.24
Various dependencies, including "golang.org/x/.."  started to update
the minimum required version,so we should follow suit for the next
release.

Note that the `//go:build` directives not necesserily have to be
updated, but it's good to keep them in sync until we have a go.mod
to control this.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-05 08:24:06 +01:00

167 lines
4.1 KiB
Go

// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
package store
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"github.com/fvbommel/sortorder"
"github.com/moby/sys/atomicwriter"
)
const (
metadataDir = "meta"
metaFile = "meta.json"
)
type metadataStore struct {
root string
config Config
}
func (s *metadataStore) contextDir(id contextdir) string {
return filepath.Join(s.root, string(id))
}
func (s *metadataStore) createOrUpdate(meta Metadata) error {
contextDir := s.contextDir(contextdirOf(meta.Name))
if err := os.MkdirAll(contextDir, 0o755); err != nil {
return err
}
bytes, err := json.Marshal(&meta)
if err != nil {
return err
}
return atomicwriter.WriteFile(filepath.Join(contextDir, metaFile), bytes, 0o644)
}
func parseTypedOrMap(payload []byte, getter TypeGetter) (any, error) {
if len(payload) == 0 || string(payload) == "null" {
return nil, nil
}
if getter == nil {
var res map[string]any
if err := json.Unmarshal(payload, &res); err != nil {
return nil, err
}
return res, nil
}
typed := getter()
if err := json.Unmarshal(payload, typed); err != nil {
return nil, err
}
return reflect.ValueOf(typed).Elem().Interface(), nil
}
func (s *metadataStore) get(name string) (Metadata, error) {
m, err := s.getByID(contextdirOf(name))
if err != nil {
return m, fmt.Errorf("context %q: %w", name, err)
}
return m, nil
}
func (s *metadataStore) getByID(id contextdir) (Metadata, error) {
fileName := filepath.Join(s.contextDir(id), metaFile)
bytes, err := os.ReadFile(fileName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return Metadata{}, notFound(fmt.Errorf("context not found: %w", err))
}
return Metadata{}, err
}
var untyped untypedContextMetadata
r := Metadata{
Endpoints: make(map[string]any),
}
if err := json.Unmarshal(bytes, &untyped); err != nil {
return Metadata{}, fmt.Errorf("parsing %s: %v", fileName, err)
}
r.Name = untyped.Name
if r.Metadata, err = parseTypedOrMap(untyped.Metadata, s.config.contextType); err != nil {
return Metadata{}, fmt.Errorf("parsing %s: %v", fileName, err)
}
for k, v := range untyped.Endpoints {
if r.Endpoints[k], err = parseTypedOrMap(v, s.config.endpointTypes[k]); err != nil {
return Metadata{}, fmt.Errorf("parsing %s: %v", fileName, err)
}
}
return r, err
}
func (s *metadataStore) remove(name string) error {
if err := os.RemoveAll(s.contextDir(contextdirOf(name))); err != nil {
return fmt.Errorf("failed to remove metadata: %w", err)
}
return nil
}
func (s *metadataStore) list() ([]Metadata, error) {
ctxDirs, err := listRecursivelyMetadataDirs(s.root)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, err
}
res := make([]Metadata, 0, len(ctxDirs))
for _, dir := range ctxDirs {
c, err := s.getByID(contextdir(dir))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return nil, fmt.Errorf("failed to read metadata: %w", err)
}
res = append(res, c)
}
sort.Slice(res, func(i, j int) bool {
return sortorder.NaturalLess(res[i].Name, res[j].Name)
})
return res, nil
}
func isContextDir(path string) bool {
s, err := os.Stat(filepath.Join(path, metaFile))
if err != nil {
return false
}
return !s.IsDir()
}
func listRecursivelyMetadataDirs(root string) ([]string, error) {
fis, err := os.ReadDir(root)
if err != nil {
return nil, err
}
var result []string
for _, fi := range fis {
if fi.IsDir() {
if isContextDir(filepath.Join(root, fi.Name())) {
result = append(result, fi.Name())
}
subs, err := listRecursivelyMetadataDirs(filepath.Join(root, fi.Name()))
if err != nil {
return nil, err
}
for _, s := range subs {
result = append(result, filepath.Join(fi.Name(), s))
}
}
}
return result, nil
}
type untypedContextMetadata struct {
Metadata json.RawMessage `json:"metadata,omitempty"`
Endpoints map[string]json.RawMessage `json:"endpoints,omitempty"`
Name string `json:"name,omitempty"`
}