github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/context/store/store.go (about)

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