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