mirror of https://github.com/docker/cli.git
502 lines
13 KiB
Go
502 lines
13 KiB
Go
package store
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"bufio"
|
|
"bytes"
|
|
_ "crypto/sha256" // ensure ids can be computed
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/errdefs"
|
|
"github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
|
|
|
|
var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern)
|
|
|
|
// Store provides a context store for easily remembering endpoints configuration
|
|
type Store interface {
|
|
Reader
|
|
Lister
|
|
Writer
|
|
StorageInfoProvider
|
|
}
|
|
|
|
// Reader provides read-only (without list) access to context data
|
|
type Reader interface {
|
|
GetMetadata(name string) (Metadata, error)
|
|
ListTLSFiles(name string) (map[string]EndpointFiles, error)
|
|
GetTLSData(contextName, endpointName, fileName string) ([]byte, error)
|
|
}
|
|
|
|
// Lister provides listing of contexts
|
|
type Lister interface {
|
|
List() ([]Metadata, error)
|
|
}
|
|
|
|
// ReaderLister combines Reader and Lister interfaces
|
|
type ReaderLister interface {
|
|
Reader
|
|
Lister
|
|
}
|
|
|
|
// StorageInfoProvider provides more information about storage details of contexts
|
|
type StorageInfoProvider interface {
|
|
GetStorageInfo(contextName string) StorageInfo
|
|
}
|
|
|
|
// Writer provides write access to context data
|
|
type Writer interface {
|
|
CreateOrUpdate(meta Metadata) error
|
|
Remove(name string) error
|
|
ResetTLSMaterial(name string, data *ContextTLSData) error
|
|
ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error
|
|
}
|
|
|
|
// ReaderWriter combines Reader and Writer interfaces
|
|
type ReaderWriter interface {
|
|
Reader
|
|
Writer
|
|
}
|
|
|
|
// Metadata contains metadata about a context and its endpoints
|
|
type Metadata struct {
|
|
Name string `json:",omitempty"`
|
|
Metadata any `json:",omitempty"`
|
|
Endpoints map[string]any `json:",omitempty"`
|
|
}
|
|
|
|
// StorageInfo contains data about where a given context is stored
|
|
type StorageInfo struct {
|
|
MetadataPath string
|
|
TLSPath string
|
|
}
|
|
|
|
// EndpointTLSData represents tls data for a given endpoint
|
|
type EndpointTLSData struct {
|
|
Files map[string][]byte
|
|
}
|
|
|
|
// ContextTLSData represents tls data for a whole context
|
|
type ContextTLSData struct {
|
|
Endpoints map[string]EndpointTLSData
|
|
}
|
|
|
|
// New creates a store from a given directory.
|
|
// If the directory does not exist or is empty, initialize it
|
|
func New(dir string, cfg Config) *ContextStore {
|
|
metaRoot := filepath.Join(dir, metadataDir)
|
|
tlsRoot := filepath.Join(dir, tlsDir)
|
|
|
|
return &ContextStore{
|
|
meta: &metadataStore{
|
|
root: metaRoot,
|
|
config: cfg,
|
|
},
|
|
tls: &tlsStore{
|
|
root: tlsRoot,
|
|
},
|
|
}
|
|
}
|
|
|
|
// ContextStore implements Store.
|
|
type ContextStore struct {
|
|
meta *metadataStore
|
|
tls *tlsStore
|
|
}
|
|
|
|
// List return all contexts.
|
|
func (s *ContextStore) List() ([]Metadata, error) {
|
|
return s.meta.list()
|
|
}
|
|
|
|
// Names return Metadata names for a Lister
|
|
func Names(s Lister) ([]string, error) {
|
|
list, err := s.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names := make([]string, 0, len(list))
|
|
for _, item := range list {
|
|
names = append(names, item.Name)
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
// CreateOrUpdate creates or updates metadata for the context.
|
|
func (s *ContextStore) CreateOrUpdate(meta Metadata) error {
|
|
return s.meta.createOrUpdate(meta)
|
|
}
|
|
|
|
// Remove deletes the context with the given name, if found.
|
|
func (s *ContextStore) Remove(name string) error {
|
|
if err := s.meta.remove(name); err != nil {
|
|
return errors.Wrapf(err, "failed to remove context %s", name)
|
|
}
|
|
if err := s.tls.remove(name); err != nil {
|
|
return errors.Wrapf(err, "failed to remove context %s", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetMetadata returns the metadata for the context with the given name.
|
|
// It returns an errdefs.ErrNotFound if the context was not found.
|
|
func (s *ContextStore) GetMetadata(name string) (Metadata, error) {
|
|
return s.meta.get(name)
|
|
}
|
|
|
|
// ResetTLSMaterial removes TLS data for all endpoints in the context and replaces
|
|
// it with the new data.
|
|
func (s *ContextStore) ResetTLSMaterial(name string, data *ContextTLSData) error {
|
|
if err := s.tls.remove(name); err != nil {
|
|
return err
|
|
}
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
for ep, files := range data.Endpoints {
|
|
for fileName, data := range files.Files {
|
|
if err := s.tls.createOrUpdate(name, ep, fileName, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ResetEndpointTLSMaterial removes TLS data for the given context and endpoint,
|
|
// and replaces it with the new data.
|
|
func (s *ContextStore) ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error {
|
|
if err := s.tls.removeEndpoint(contextName, endpointName); err != nil {
|
|
return err
|
|
}
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
for fileName, data := range data.Files {
|
|
if err := s.tls.createOrUpdate(contextName, endpointName, fileName, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListTLSFiles returns the list of TLS files present for each endpoint in the
|
|
// context.
|
|
func (s *ContextStore) ListTLSFiles(name string) (map[string]EndpointFiles, error) {
|
|
return s.tls.listContextData(name)
|
|
}
|
|
|
|
// GetTLSData reads, and returns the content of the given fileName for an endpoint.
|
|
// It returns an errdefs.ErrNotFound if the file was not found.
|
|
func (s *ContextStore) GetTLSData(contextName, endpointName, fileName string) ([]byte, error) {
|
|
return s.tls.getData(contextName, endpointName, fileName)
|
|
}
|
|
|
|
// GetStorageInfo returns the paths where the Metadata and TLS data are stored
|
|
// for the context.
|
|
func (s *ContextStore) GetStorageInfo(contextName string) StorageInfo {
|
|
return StorageInfo{
|
|
MetadataPath: s.meta.contextDir(contextdirOf(contextName)),
|
|
TLSPath: s.tls.contextDir(contextName),
|
|
}
|
|
}
|
|
|
|
// ValidateContextName checks a context name is valid.
|
|
func ValidateContextName(name string) error {
|
|
if name == "" {
|
|
return errors.New("context name cannot be empty")
|
|
}
|
|
if name == "default" {
|
|
return errors.New(`"default" is a reserved context name`)
|
|
}
|
|
if !restrictedNameRegEx.MatchString(name) {
|
|
return errors.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Export exports an existing namespace into an opaque data stream
|
|
// This stream is actually a tarball containing context metadata and TLS materials, but it does
|
|
// not map 1:1 the layout of the context store (don't try to restore it manually without calling store.Import)
|
|
func Export(name string, s Reader) io.ReadCloser {
|
|
reader, writer := io.Pipe()
|
|
go func() {
|
|
tw := tar.NewWriter(writer)
|
|
defer tw.Close()
|
|
defer writer.Close()
|
|
meta, err := s.GetMetadata(name)
|
|
if err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
metaBytes, err := json.Marshal(&meta)
|
|
if err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
if err = tw.WriteHeader(&tar.Header{
|
|
Name: metaFile,
|
|
Mode: 0o644,
|
|
Size: int64(len(metaBytes)),
|
|
}); err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
if _, err = tw.Write(metaBytes); err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
tlsFiles, err := s.ListTLSFiles(name)
|
|
if err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
if err = tw.WriteHeader(&tar.Header{
|
|
Name: "tls",
|
|
Mode: 0o700,
|
|
Size: 0,
|
|
Typeflag: tar.TypeDir,
|
|
}); err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
for endpointName, endpointFiles := range tlsFiles {
|
|
if err = tw.WriteHeader(&tar.Header{
|
|
Name: path.Join("tls", endpointName),
|
|
Mode: 0o700,
|
|
Size: 0,
|
|
Typeflag: tar.TypeDir,
|
|
}); err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
for _, fileName := range endpointFiles {
|
|
data, err := s.GetTLSData(name, endpointName, fileName)
|
|
if err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
if err = tw.WriteHeader(&tar.Header{
|
|
Name: path.Join("tls", endpointName, fileName),
|
|
Mode: 0o600,
|
|
Size: int64(len(data)),
|
|
}); err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
if _, err = tw.Write(data); err != nil {
|
|
writer.CloseWithError(err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return reader
|
|
}
|
|
|
|
const (
|
|
maxAllowedFileSizeToImport int64 = 10 << 20
|
|
zipType string = "application/zip"
|
|
)
|
|
|
|
func getImportContentType(r *bufio.Reader) (string, error) {
|
|
head, err := r.Peek(512)
|
|
if err != nil && err != io.EOF {
|
|
return "", err
|
|
}
|
|
|
|
return http.DetectContentType(head), nil
|
|
}
|
|
|
|
// Import imports an exported context into a store
|
|
func Import(name string, s Writer, reader io.Reader) error {
|
|
// Buffered reader will not advance the buffer, needed to determine content type
|
|
r := bufio.NewReader(reader)
|
|
|
|
importContentType, err := getImportContentType(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch importContentType {
|
|
case zipType:
|
|
return importZip(name, s, r)
|
|
default:
|
|
// Assume it's a TAR (TAR does not have a "magic number")
|
|
return importTar(name, s, r)
|
|
}
|
|
}
|
|
|
|
func isValidFilePath(p string) error {
|
|
if p != metaFile && !strings.HasPrefix(p, "tls/") {
|
|
return errors.New("unexpected context file")
|
|
}
|
|
if path.Clean(p) != p {
|
|
return errors.New("unexpected path format")
|
|
}
|
|
if strings.Contains(p, `\`) {
|
|
return errors.New(`unexpected '\' in path`)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func importTar(name string, s Writer, reader io.Reader) error {
|
|
tr := tar.NewReader(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
|
|
tlsData := ContextTLSData{
|
|
Endpoints: map[string]EndpointTLSData{},
|
|
}
|
|
var importedMetaFile bool
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hdr.Typeflag != tar.TypeReg {
|
|
// skip this entry, only taking files into account
|
|
continue
|
|
}
|
|
if err := isValidFilePath(hdr.Name); err != nil {
|
|
return errors.Wrap(err, hdr.Name)
|
|
}
|
|
if hdr.Name == metaFile {
|
|
data, err := io.ReadAll(tr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
meta, err := parseMetadata(data, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.CreateOrUpdate(meta); err != nil {
|
|
return err
|
|
}
|
|
importedMetaFile = true
|
|
} else if strings.HasPrefix(hdr.Name, "tls/") {
|
|
data, err := io.ReadAll(tr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := importEndpointTLS(&tlsData, hdr.Name, data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if !importedMetaFile {
|
|
return errdefs.InvalidParameter(errors.New("invalid context: no metadata found"))
|
|
}
|
|
return s.ResetTLSMaterial(name, &tlsData)
|
|
}
|
|
|
|
func importZip(name string, s Writer, reader io.Reader) error {
|
|
body, err := io.ReadAll(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tlsData := ContextTLSData{
|
|
Endpoints: map[string]EndpointTLSData{},
|
|
}
|
|
|
|
var importedMetaFile bool
|
|
for _, zf := range zr.File {
|
|
fi := zf.FileInfo()
|
|
if !fi.Mode().IsRegular() {
|
|
// skip this entry, only taking regular files into account
|
|
continue
|
|
}
|
|
if err := isValidFilePath(zf.Name); err != nil {
|
|
return errors.Wrap(err, zf.Name)
|
|
}
|
|
if zf.Name == metaFile {
|
|
f, err := zf.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := io.ReadAll(&LimitedReader{R: f, N: maxAllowedFileSizeToImport})
|
|
defer f.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
meta, err := parseMetadata(data, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.CreateOrUpdate(meta); err != nil {
|
|
return err
|
|
}
|
|
importedMetaFile = true
|
|
} else if strings.HasPrefix(zf.Name, "tls/") {
|
|
f, err := zf.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err := io.ReadAll(f)
|
|
defer f.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = importEndpointTLS(&tlsData, zf.Name, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if !importedMetaFile {
|
|
return errdefs.InvalidParameter(errors.New("invalid context: no metadata found"))
|
|
}
|
|
return s.ResetTLSMaterial(name, &tlsData)
|
|
}
|
|
|
|
func parseMetadata(data []byte, name string) (Metadata, error) {
|
|
var meta Metadata
|
|
if err := json.Unmarshal(data, &meta); err != nil {
|
|
return meta, err
|
|
}
|
|
if err := ValidateContextName(name); err != nil {
|
|
return Metadata{}, err
|
|
}
|
|
meta.Name = name
|
|
return meta, nil
|
|
}
|
|
|
|
func importEndpointTLS(tlsData *ContextTLSData, tlsPath string, data []byte) error {
|
|
parts := strings.SplitN(strings.TrimPrefix(tlsPath, "tls/"), "/", 2)
|
|
if len(parts) != 2 {
|
|
// TLS endpoints require archived file directory with 2 layers
|
|
// i.e. tls/{endpointName}/{fileName}
|
|
return errors.New("archive format is invalid")
|
|
}
|
|
|
|
epName := parts[0]
|
|
fileName := parts[1]
|
|
if _, ok := tlsData.Endpoints[epName]; !ok {
|
|
tlsData.Endpoints[epName] = EndpointTLSData{
|
|
Files: map[string][]byte{},
|
|
}
|
|
}
|
|
tlsData.Endpoints[epName].Files[fileName] = data
|
|
return nil
|
|
}
|
|
|
|
type contextdir string
|
|
|
|
func contextdirOf(name string) contextdir {
|
|
return contextdir(digest.FromString(name).Encoded())
|
|
}
|