github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/context/store/store.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  	"archive/tar"
     8  	"archive/zip"
     9  	"bufio"
    10  	"bytes"
    11  	_ "crypto/sha256" // ensure ids can be computed
    12  	"encoding/json"
    13  	"io"
    14  	"net/http"
    15  	"path"
    16  	"path/filepath"
    17  	"regexp"
    18  	"strings"
    19  
    20  	"github.com/docker/docker/errdefs"
    21  	"github.com/opencontainers/go-digest"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
    26  
    27  var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern)
    28  
    29  // Store provides a context store for easily remembering endpoints configuration
    30  type Store interface {
    31  	Reader
    32  	Lister
    33  	Writer
    34  	StorageInfoProvider
    35  }
    36  
    37  // Reader provides read-only (without list) access to context data
    38  type Reader interface {
    39  	GetMetadata(name string) (Metadata, error)
    40  	ListTLSFiles(name string) (map[string]EndpointFiles, error)
    41  	GetTLSData(contextName, endpointName, fileName string) ([]byte, error)
    42  }
    43  
    44  // Lister provides listing of contexts
    45  type Lister interface {
    46  	List() ([]Metadata, error)
    47  }
    48  
    49  // ReaderLister combines Reader and Lister interfaces
    50  type ReaderLister interface {
    51  	Reader
    52  	Lister
    53  }
    54  
    55  // StorageInfoProvider provides more information about storage details of contexts
    56  type StorageInfoProvider interface {
    57  	GetStorageInfo(contextName string) StorageInfo
    58  }
    59  
    60  // Writer provides write access to context data
    61  type Writer interface {
    62  	CreateOrUpdate(meta Metadata) error
    63  	Remove(name string) error
    64  	ResetTLSMaterial(name string, data *ContextTLSData) error
    65  	ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error
    66  }
    67  
    68  // ReaderWriter combines Reader and Writer interfaces
    69  type ReaderWriter interface {
    70  	Reader
    71  	Writer
    72  }
    73  
    74  // Metadata contains metadata about a context and its endpoints
    75  type Metadata struct {
    76  	Name      string         `json:",omitempty"`
    77  	Metadata  any            `json:",omitempty"`
    78  	Endpoints map[string]any `json:",omitempty"`
    79  }
    80  
    81  // StorageInfo contains data about where a given context is stored
    82  type StorageInfo struct {
    83  	MetadataPath string
    84  	TLSPath      string
    85  }
    86  
    87  // EndpointTLSData represents tls data for a given endpoint
    88  type EndpointTLSData struct {
    89  	Files map[string][]byte
    90  }
    91  
    92  // ContextTLSData represents tls data for a whole context
    93  type ContextTLSData struct {
    94  	Endpoints map[string]EndpointTLSData
    95  }
    96  
    97  // New creates a store from a given directory.
    98  // If the directory does not exist or is empty, initialize it
    99  func New(dir string, cfg Config) *ContextStore {
   100  	metaRoot := filepath.Join(dir, metadataDir)
   101  	tlsRoot := filepath.Join(dir, tlsDir)
   102  
   103  	return &ContextStore{
   104  		meta: &metadataStore{
   105  			root:   metaRoot,
   106  			config: cfg,
   107  		},
   108  		tls: &tlsStore{
   109  			root: tlsRoot,
   110  		},
   111  	}
   112  }
   113  
   114  // ContextStore implements Store.
   115  type ContextStore struct {
   116  	meta *metadataStore
   117  	tls  *tlsStore
   118  }
   119  
   120  // List return all contexts.
   121  func (s *ContextStore) List() ([]Metadata, error) {
   122  	return s.meta.list()
   123  }
   124  
   125  // Names return Metadata names for a Lister
   126  func Names(s Lister) ([]string, error) {
   127  	list, err := s.List()
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	names := make([]string, 0, len(list))
   132  	for _, item := range list {
   133  		names = append(names, item.Name)
   134  	}
   135  	return names, nil
   136  }
   137  
   138  // CreateOrUpdate creates or updates metadata for the context.
   139  func (s *ContextStore) CreateOrUpdate(meta Metadata) error {
   140  	return s.meta.createOrUpdate(meta)
   141  }
   142  
   143  // Remove deletes the context with the given name, if found.
   144  func (s *ContextStore) Remove(name string) error {
   145  	if err := s.meta.remove(name); err != nil {
   146  		return errors.Wrapf(err, "failed to remove context %s", name)
   147  	}
   148  	if err := s.tls.remove(name); err != nil {
   149  		return errors.Wrapf(err, "failed to remove context %s", name)
   150  	}
   151  	return nil
   152  }
   153  
   154  // GetMetadata returns the metadata for the context with the given name.
   155  // It returns an errdefs.ErrNotFound if the context was not found.
   156  func (s *ContextStore) GetMetadata(name string) (Metadata, error) {
   157  	return s.meta.get(name)
   158  }
   159  
   160  // ResetTLSMaterial removes TLS data for all endpoints in the context and replaces
   161  // it with the new data.
   162  func (s *ContextStore) ResetTLSMaterial(name string, data *ContextTLSData) error {
   163  	if err := s.tls.remove(name); err != nil {
   164  		return err
   165  	}
   166  	if data == nil {
   167  		return nil
   168  	}
   169  	for ep, files := range data.Endpoints {
   170  		for fileName, data := range files.Files {
   171  			if err := s.tls.createOrUpdate(name, ep, fileName, data); err != nil {
   172  				return err
   173  			}
   174  		}
   175  	}
   176  	return nil
   177  }
   178  
   179  // ResetEndpointTLSMaterial removes TLS data for the given context and endpoint,
   180  // and replaces it with the new data.
   181  func (s *ContextStore) ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error {
   182  	if err := s.tls.removeEndpoint(contextName, endpointName); err != nil {
   183  		return err
   184  	}
   185  	if data == nil {
   186  		return nil
   187  	}
   188  	for fileName, data := range data.Files {
   189  		if err := s.tls.createOrUpdate(contextName, endpointName, fileName, data); err != nil {
   190  			return err
   191  		}
   192  	}
   193  	return nil
   194  }
   195  
   196  // ListTLSFiles returns the list of TLS files present for each endpoint in the
   197  // context.
   198  func (s *ContextStore) ListTLSFiles(name string) (map[string]EndpointFiles, error) {
   199  	return s.tls.listContextData(name)
   200  }
   201  
   202  // GetTLSData reads, and returns the content of the given fileName for an endpoint.
   203  // It returns an errdefs.ErrNotFound if the file was not found.
   204  func (s *ContextStore) GetTLSData(contextName, endpointName, fileName string) ([]byte, error) {
   205  	return s.tls.getData(contextName, endpointName, fileName)
   206  }
   207  
   208  // GetStorageInfo returns the paths where the Metadata and TLS data are stored
   209  // for the context.
   210  func (s *ContextStore) GetStorageInfo(contextName string) StorageInfo {
   211  	return StorageInfo{
   212  		MetadataPath: s.meta.contextDir(contextdirOf(contextName)),
   213  		TLSPath:      s.tls.contextDir(contextName),
   214  	}
   215  }
   216  
   217  // ValidateContextName checks a context name is valid.
   218  func ValidateContextName(name string) error {
   219  	if name == "" {
   220  		return errors.New("context name cannot be empty")
   221  	}
   222  	if name == "default" {
   223  		return errors.New(`"default" is a reserved context name`)
   224  	}
   225  	if !restrictedNameRegEx.MatchString(name) {
   226  		return errors.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern)
   227  	}
   228  	return nil
   229  }
   230  
   231  // Export exports an existing namespace into an opaque data stream
   232  // This stream is actually a tarball containing context metadata and TLS materials, but it does
   233  // not map 1:1 the layout of the context store (don't try to restore it manually without calling store.Import)
   234  func Export(name string, s Reader) io.ReadCloser {
   235  	reader, writer := io.Pipe()
   236  	go func() {
   237  		tw := tar.NewWriter(writer)
   238  		defer tw.Close()
   239  		defer writer.Close()
   240  		meta, err := s.GetMetadata(name)
   241  		if err != nil {
   242  			writer.CloseWithError(err)
   243  			return
   244  		}
   245  		metaBytes, err := json.Marshal(&meta)
   246  		if err != nil {
   247  			writer.CloseWithError(err)
   248  			return
   249  		}
   250  		if err = tw.WriteHeader(&tar.Header{
   251  			Name: metaFile,
   252  			Mode: 0o644,
   253  			Size: int64(len(metaBytes)),
   254  		}); err != nil {
   255  			writer.CloseWithError(err)
   256  			return
   257  		}
   258  		if _, err = tw.Write(metaBytes); err != nil {
   259  			writer.CloseWithError(err)
   260  			return
   261  		}
   262  		tlsFiles, err := s.ListTLSFiles(name)
   263  		if err != nil {
   264  			writer.CloseWithError(err)
   265  			return
   266  		}
   267  		if err = tw.WriteHeader(&tar.Header{
   268  			Name:     "tls",
   269  			Mode:     0o700,
   270  			Size:     0,
   271  			Typeflag: tar.TypeDir,
   272  		}); err != nil {
   273  			writer.CloseWithError(err)
   274  			return
   275  		}
   276  		for endpointName, endpointFiles := range tlsFiles {
   277  			if err = tw.WriteHeader(&tar.Header{
   278  				Name:     path.Join("tls", endpointName),
   279  				Mode:     0o700,
   280  				Size:     0,
   281  				Typeflag: tar.TypeDir,
   282  			}); err != nil {
   283  				writer.CloseWithError(err)
   284  				return
   285  			}
   286  			for _, fileName := range endpointFiles {
   287  				data, err := s.GetTLSData(name, endpointName, fileName)
   288  				if err != nil {
   289  					writer.CloseWithError(err)
   290  					return
   291  				}
   292  				if err = tw.WriteHeader(&tar.Header{
   293  					Name: path.Join("tls", endpointName, fileName),
   294  					Mode: 0o600,
   295  					Size: int64(len(data)),
   296  				}); err != nil {
   297  					writer.CloseWithError(err)
   298  					return
   299  				}
   300  				if _, err = tw.Write(data); err != nil {
   301  					writer.CloseWithError(err)
   302  					return
   303  				}
   304  			}
   305  		}
   306  	}()
   307  	return reader
   308  }
   309  
   310  const (
   311  	maxAllowedFileSizeToImport int64  = 10 << 20
   312  	zipType                    string = "application/zip"
   313  )
   314  
   315  func getImportContentType(r *bufio.Reader) (string, error) {
   316  	head, err := r.Peek(512)
   317  	if err != nil && err != io.EOF {
   318  		return "", err
   319  	}
   320  
   321  	return http.DetectContentType(head), nil
   322  }
   323  
   324  // Import imports an exported context into a store
   325  func Import(name string, s Writer, reader io.Reader) error {
   326  	// Buffered reader will not advance the buffer, needed to determine content type
   327  	r := bufio.NewReader(reader)
   328  
   329  	importContentType, err := getImportContentType(r)
   330  	if err != nil {
   331  		return err
   332  	}
   333  	switch importContentType {
   334  	case zipType:
   335  		return importZip(name, s, r)
   336  	default:
   337  		// Assume it's a TAR (TAR does not have a "magic number")
   338  		return importTar(name, s, r)
   339  	}
   340  }
   341  
   342  func isValidFilePath(p string) error {
   343  	if p != metaFile && !strings.HasPrefix(p, "tls/") {
   344  		return errors.New("unexpected context file")
   345  	}
   346  	if path.Clean(p) != p {
   347  		return errors.New("unexpected path format")
   348  	}
   349  	if strings.Contains(p, `\`) {
   350  		return errors.New(`unexpected '\' in path`)
   351  	}
   352  	return nil
   353  }
   354  
   355  func importTar(name string, s Writer, reader io.Reader) error {
   356  	tr := tar.NewReader(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
   357  	tlsData := ContextTLSData{
   358  		Endpoints: map[string]EndpointTLSData{},
   359  	}
   360  	var importedMetaFile bool
   361  	for {
   362  		hdr, err := tr.Next()
   363  		if err == io.EOF {
   364  			break
   365  		}
   366  		if err != nil {
   367  			return err
   368  		}
   369  		if hdr.Typeflag != tar.TypeReg {
   370  			// skip this entry, only taking files into account
   371  			continue
   372  		}
   373  		if err := isValidFilePath(hdr.Name); err != nil {
   374  			return errors.Wrap(err, hdr.Name)
   375  		}
   376  		if hdr.Name == metaFile {
   377  			data, err := io.ReadAll(tr)
   378  			if err != nil {
   379  				return err
   380  			}
   381  			meta, err := parseMetadata(data, name)
   382  			if err != nil {
   383  				return err
   384  			}
   385  			if err := s.CreateOrUpdate(meta); err != nil {
   386  				return err
   387  			}
   388  			importedMetaFile = true
   389  		} else if strings.HasPrefix(hdr.Name, "tls/") {
   390  			data, err := io.ReadAll(tr)
   391  			if err != nil {
   392  				return err
   393  			}
   394  			if err := importEndpointTLS(&tlsData, hdr.Name, data); err != nil {
   395  				return err
   396  			}
   397  		}
   398  	}
   399  	if !importedMetaFile {
   400  		return errdefs.InvalidParameter(errors.New("invalid context: no metadata found"))
   401  	}
   402  	return s.ResetTLSMaterial(name, &tlsData)
   403  }
   404  
   405  func importZip(name string, s Writer, reader io.Reader) error {
   406  	body, err := io.ReadAll(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
   407  	if err != nil {
   408  		return err
   409  	}
   410  	zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
   411  	if err != nil {
   412  		return err
   413  	}
   414  	tlsData := ContextTLSData{
   415  		Endpoints: map[string]EndpointTLSData{},
   416  	}
   417  
   418  	var importedMetaFile bool
   419  	for _, zf := range zr.File {
   420  		fi := zf.FileInfo()
   421  		if !fi.Mode().IsRegular() {
   422  			// skip this entry, only taking regular files into account
   423  			continue
   424  		}
   425  		if err := isValidFilePath(zf.Name); err != nil {
   426  			return errors.Wrap(err, zf.Name)
   427  		}
   428  		if zf.Name == metaFile {
   429  			f, err := zf.Open()
   430  			if err != nil {
   431  				return err
   432  			}
   433  
   434  			data, err := io.ReadAll(&LimitedReader{R: f, N: maxAllowedFileSizeToImport})
   435  			defer f.Close()
   436  			if err != nil {
   437  				return err
   438  			}
   439  			meta, err := parseMetadata(data, name)
   440  			if err != nil {
   441  				return err
   442  			}
   443  			if err := s.CreateOrUpdate(meta); err != nil {
   444  				return err
   445  			}
   446  			importedMetaFile = true
   447  		} else if strings.HasPrefix(zf.Name, "tls/") {
   448  			f, err := zf.Open()
   449  			if err != nil {
   450  				return err
   451  			}
   452  			data, err := io.ReadAll(f)
   453  			defer f.Close()
   454  			if err != nil {
   455  				return err
   456  			}
   457  			err = importEndpointTLS(&tlsData, zf.Name, data)
   458  			if err != nil {
   459  				return err
   460  			}
   461  		}
   462  	}
   463  	if !importedMetaFile {
   464  		return errdefs.InvalidParameter(errors.New("invalid context: no metadata found"))
   465  	}
   466  	return s.ResetTLSMaterial(name, &tlsData)
   467  }
   468  
   469  func parseMetadata(data []byte, name string) (Metadata, error) {
   470  	var meta Metadata
   471  	if err := json.Unmarshal(data, &meta); err != nil {
   472  		return meta, err
   473  	}
   474  	if err := ValidateContextName(name); err != nil {
   475  		return Metadata{}, err
   476  	}
   477  	meta.Name = name
   478  	return meta, nil
   479  }
   480  
   481  func importEndpointTLS(tlsData *ContextTLSData, tlsPath string, data []byte) error {
   482  	parts := strings.SplitN(strings.TrimPrefix(tlsPath, "tls/"), "/", 2)
   483  	if len(parts) != 2 {
   484  		// TLS endpoints require archived file directory with 2 layers
   485  		// i.e. tls/{endpointName}/{fileName}
   486  		return errors.New("archive format is invalid")
   487  	}
   488  
   489  	epName := parts[0]
   490  	fileName := parts[1]
   491  	if _, ok := tlsData.Endpoints[epName]; !ok {
   492  		tlsData.Endpoints[epName] = EndpointTLSData{
   493  			Files: map[string][]byte{},
   494  		}
   495  	}
   496  	tlsData.Endpoints[epName].Files[fileName] = data
   497  	return nil
   498  }
   499  
   500  type contextdir string
   501  
   502  func contextdirOf(name string) contextdir {
   503  	return contextdir(digest.FromString(name).Encoded())
   504  }