github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/context/store/metadatastore.go (about)

     1  // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
     2  //go:build go1.19
     3  
     4  package store
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"sort"
    13  
    14  	"github.com/docker/docker/errdefs"
    15  	"github.com/docker/docker/pkg/ioutils"
    16  	"github.com/fvbommel/sortorder"
    17  	"github.com/pkg/errors"
    18  )
    19  
    20  const (
    21  	metadataDir = "meta"
    22  	metaFile    = "meta.json"
    23  )
    24  
    25  type metadataStore struct {
    26  	root   string
    27  	config Config
    28  }
    29  
    30  func (s *metadataStore) contextDir(id contextdir) string {
    31  	return filepath.Join(s.root, string(id))
    32  }
    33  
    34  func (s *metadataStore) createOrUpdate(meta Metadata) error {
    35  	contextDir := s.contextDir(contextdirOf(meta.Name))
    36  	if err := os.MkdirAll(contextDir, 0o755); err != nil {
    37  		return err
    38  	}
    39  	bytes, err := json.Marshal(&meta)
    40  	if err != nil {
    41  		return err
    42  	}
    43  	return ioutils.AtomicWriteFile(filepath.Join(contextDir, metaFile), bytes, 0o644)
    44  }
    45  
    46  func parseTypedOrMap(payload []byte, getter TypeGetter) (any, error) {
    47  	if len(payload) == 0 || string(payload) == "null" {
    48  		return nil, nil
    49  	}
    50  	if getter == nil {
    51  		var res map[string]any
    52  		if err := json.Unmarshal(payload, &res); err != nil {
    53  			return nil, err
    54  		}
    55  		return res, nil
    56  	}
    57  	typed := getter()
    58  	if err := json.Unmarshal(payload, typed); err != nil {
    59  		return nil, err
    60  	}
    61  	return reflect.ValueOf(typed).Elem().Interface(), nil
    62  }
    63  
    64  func (s *metadataStore) get(name string) (Metadata, error) {
    65  	m, err := s.getByID(contextdirOf(name))
    66  	if err != nil {
    67  		return m, errors.Wrapf(err, "context %q", name)
    68  	}
    69  	return m, nil
    70  }
    71  
    72  func (s *metadataStore) getByID(id contextdir) (Metadata, error) {
    73  	fileName := filepath.Join(s.contextDir(id), metaFile)
    74  	bytes, err := os.ReadFile(fileName)
    75  	if err != nil {
    76  		if errors.Is(err, os.ErrNotExist) {
    77  			return Metadata{}, errdefs.NotFound(errors.Wrap(err, "context not found"))
    78  		}
    79  		return Metadata{}, err
    80  	}
    81  	var untyped untypedContextMetadata
    82  	r := Metadata{
    83  		Endpoints: make(map[string]any),
    84  	}
    85  	if err := json.Unmarshal(bytes, &untyped); err != nil {
    86  		return Metadata{}, fmt.Errorf("parsing %s: %v", fileName, err)
    87  	}
    88  	r.Name = untyped.Name
    89  	if r.Metadata, err = parseTypedOrMap(untyped.Metadata, s.config.contextType); err != nil {
    90  		return Metadata{}, fmt.Errorf("parsing %s: %v", fileName, err)
    91  	}
    92  	for k, v := range untyped.Endpoints {
    93  		if r.Endpoints[k], err = parseTypedOrMap(v, s.config.endpointTypes[k]); err != nil {
    94  			return Metadata{}, fmt.Errorf("parsing %s: %v", fileName, err)
    95  		}
    96  	}
    97  	return r, err
    98  }
    99  
   100  func (s *metadataStore) remove(name string) error {
   101  	if err := os.RemoveAll(s.contextDir(contextdirOf(name))); err != nil {
   102  		return errors.Wrapf(err, "failed to remove metadata")
   103  	}
   104  	return nil
   105  }
   106  
   107  func (s *metadataStore) list() ([]Metadata, error) {
   108  	ctxDirs, err := listRecursivelyMetadataDirs(s.root)
   109  	if err != nil {
   110  		if errors.Is(err, os.ErrNotExist) {
   111  			return nil, nil
   112  		}
   113  		return nil, err
   114  	}
   115  	res := make([]Metadata, 0, len(ctxDirs))
   116  	for _, dir := range ctxDirs {
   117  		c, err := s.getByID(contextdir(dir))
   118  		if err != nil {
   119  			if errors.Is(err, os.ErrNotExist) {
   120  				continue
   121  			}
   122  			return nil, errors.Wrap(err, "failed to read metadata")
   123  		}
   124  		res = append(res, c)
   125  	}
   126  	sort.Slice(res, func(i, j int) bool {
   127  		return sortorder.NaturalLess(res[i].Name, res[j].Name)
   128  	})
   129  	return res, nil
   130  }
   131  
   132  func isContextDir(path string) bool {
   133  	s, err := os.Stat(filepath.Join(path, metaFile))
   134  	if err != nil {
   135  		return false
   136  	}
   137  	return !s.IsDir()
   138  }
   139  
   140  func listRecursivelyMetadataDirs(root string) ([]string, error) {
   141  	fis, err := os.ReadDir(root)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	var result []string
   146  	for _, fi := range fis {
   147  		if fi.IsDir() {
   148  			if isContextDir(filepath.Join(root, fi.Name())) {
   149  				result = append(result, fi.Name())
   150  			}
   151  			subs, err := listRecursivelyMetadataDirs(filepath.Join(root, fi.Name()))
   152  			if err != nil {
   153  				return nil, err
   154  			}
   155  			for _, s := range subs {
   156  				result = append(result, filepath.Join(fi.Name(), s))
   157  			}
   158  		}
   159  	}
   160  	return result, nil
   161  }
   162  
   163  type untypedContextMetadata struct {
   164  	Metadata  json.RawMessage            `json:"metadata,omitempty"`
   165  	Endpoints map[string]json.RawMessage `json:"endpoints,omitempty"`
   166  	Name      string                     `json:"name,omitempty"`
   167  }