DockerCLI/cli/context/store/store.go

341 lines
8.8 KiB
Go

package store
import (
"archive/tar"
_ "crypto/sha256" // ensure ids can be computed
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"path"
"path/filepath"
"strings"
"github.com/opencontainers/go-digest"
)
// Store provides a context store for easily remembering endpoints configuration
type Store interface {
ListContexts() ([]ContextMetadata, error)
CreateOrUpdateContext(meta ContextMetadata) error
RemoveContext(name string) error
GetContextMetadata(name string) (ContextMetadata, error)
ResetContextTLSMaterial(name string, data *ContextTLSData) error
ResetContextEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error
ListContextTLSFiles(name string) (map[string]EndpointFiles, error)
GetContextTLSData(contextName, endpointName, fileName string) ([]byte, error)
GetContextStorageInfo(contextName string) ContextStorageInfo
}
// ContextMetadata contains metadata about a context and its endpoints
type ContextMetadata struct {
Name string `json:",omitempty"`
Metadata interface{} `json:",omitempty"`
Endpoints map[string]interface{} `json:",omitempty"`
}
// ContextStorageInfo contains data about where a given context is stored
type ContextStorageInfo 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) Store {
metaRoot := filepath.Join(dir, metadataDir)
tlsRoot := filepath.Join(dir, tlsDir)
return &store{
meta: &metadataStore{
root: metaRoot,
config: cfg,
},
tls: &tlsStore{
root: tlsRoot,
},
}
}
type store struct {
meta *metadataStore
tls *tlsStore
}
func (s *store) ListContexts() ([]ContextMetadata, error) {
return s.meta.list()
}
func (s *store) CreateOrUpdateContext(meta ContextMetadata) error {
return s.meta.createOrUpdate(meta)
}
func (s *store) RemoveContext(name string) error {
id := contextdirOf(name)
if err := s.meta.remove(id); err != nil {
return patchErrContextName(err, name)
}
return patchErrContextName(s.tls.removeAllContextData(id), name)
}
func (s *store) GetContextMetadata(name string) (ContextMetadata, error) {
res, err := s.meta.get(contextdirOf(name))
patchErrContextName(err, name)
return res, err
}
func (s *store) ResetContextTLSMaterial(name string, data *ContextTLSData) error {
id := contextdirOf(name)
if err := s.tls.removeAllContextData(id); err != nil {
return patchErrContextName(err, name)
}
if data == nil {
return nil
}
for ep, files := range data.Endpoints {
for fileName, data := range files.Files {
if err := s.tls.createOrUpdate(id, ep, fileName, data); err != nil {
return patchErrContextName(err, name)
}
}
}
return nil
}
func (s *store) ResetContextEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error {
id := contextdirOf(contextName)
if err := s.tls.removeAllEndpointData(id, endpointName); err != nil {
return patchErrContextName(err, contextName)
}
if data == nil {
return nil
}
for fileName, data := range data.Files {
if err := s.tls.createOrUpdate(id, endpointName, fileName, data); err != nil {
return patchErrContextName(err, contextName)
}
}
return nil
}
func (s *store) ListContextTLSFiles(name string) (map[string]EndpointFiles, error) {
res, err := s.tls.listContextData(contextdirOf(name))
return res, patchErrContextName(err, name)
}
func (s *store) GetContextTLSData(contextName, endpointName, fileName string) ([]byte, error) {
res, err := s.tls.getData(contextdirOf(contextName), endpointName, fileName)
return res, patchErrContextName(err, contextName)
}
func (s *store) GetContextStorageInfo(contextName string) ContextStorageInfo {
dir := contextdirOf(contextName)
return ContextStorageInfo{
MetadataPath: s.meta.contextDir(dir),
TLSPath: s.tls.contextDir(dir),
}
}
// 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 Store) io.ReadCloser {
reader, writer := io.Pipe()
go func() {
tw := tar.NewWriter(writer)
defer tw.Close()
defer writer.Close()
meta, err := s.GetContextMetadata(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: 0644,
Size: int64(len(metaBytes)),
}); err != nil {
writer.CloseWithError(err)
return
}
if _, err = tw.Write(metaBytes); err != nil {
writer.CloseWithError(err)
return
}
tlsFiles, err := s.ListContextTLSFiles(name)
if err != nil {
writer.CloseWithError(err)
return
}
if err = tw.WriteHeader(&tar.Header{
Name: "tls",
Mode: 0700,
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: 0700,
Size: 0,
Typeflag: tar.TypeDir,
}); err != nil {
writer.CloseWithError(err)
return
}
for _, fileName := range endpointFiles {
data, err := s.GetContextTLSData(name, endpointName, fileName)
if err != nil {
writer.CloseWithError(err)
return
}
if err = tw.WriteHeader(&tar.Header{
Name: path.Join("tls", endpointName, fileName),
Mode: 0600,
Size: int64(len(data)),
}); err != nil {
writer.CloseWithError(err)
return
}
if _, err = tw.Write(data); err != nil {
writer.CloseWithError(err)
return
}
}
}
}()
return reader
}
// Import imports an exported context into a store
func Import(name string, s Store, reader io.Reader) error {
tr := tar.NewReader(reader)
tlsData := ContextTLSData{
Endpoints: map[string]EndpointTLSData{},
}
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if hdr.Typeflag == tar.TypeDir {
// skip this entry, only taking files into account
continue
}
if hdr.Name == metaFile {
data, err := ioutil.ReadAll(tr)
if err != nil {
return err
}
var meta ContextMetadata
if err := json.Unmarshal(data, &meta); err != nil {
return err
}
meta.Name = name
if err := s.CreateOrUpdateContext(meta); err != nil {
return err
}
} else if strings.HasPrefix(hdr.Name, "tls/") {
relative := strings.TrimPrefix(hdr.Name, "tls/")
parts := strings.SplitN(relative, "/", 2)
if len(parts) != 2 {
return errors.New("archive format is invalid")
}
endpointName := parts[0]
fileName := parts[1]
data, err := ioutil.ReadAll(tr)
if err != nil {
return err
}
if _, ok := tlsData.Endpoints[endpointName]; !ok {
tlsData.Endpoints[endpointName] = EndpointTLSData{
Files: map[string][]byte{},
}
}
tlsData.Endpoints[endpointName].Files[fileName] = data
}
}
return s.ResetContextTLSMaterial(name, &tlsData)
}
type setContextName interface {
setContext(name string)
}
type contextDoesNotExistError struct {
name string
}
func (e *contextDoesNotExistError) Error() string {
return fmt.Sprintf("context %q does not exist", e.name)
}
func (e *contextDoesNotExistError) setContext(name string) {
e.name = name
}
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (e *contextDoesNotExistError) NotFound() {}
type tlsDataDoesNotExistError struct {
context, endpoint, file string
}
func (e *tlsDataDoesNotExistError) Error() string {
return fmt.Sprintf("tls data for %s/%s/%s does not exist", e.context, e.endpoint, e.file)
}
func (e *tlsDataDoesNotExistError) setContext(name string) {
e.context = name
}
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
func (e *tlsDataDoesNotExistError) NotFound() {}
// IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition
func IsErrContextDoesNotExist(err error) bool {
_, ok := err.(*contextDoesNotExistError)
return ok
}
// IsErrTLSDataDoesNotExist checks if the given error is a "context does not exist" condition
func IsErrTLSDataDoesNotExist(err error) bool {
_, ok := err.(*tlsDataDoesNotExistError)
return ok
}
type contextdir string
func contextdirOf(name string) contextdir {
return contextdir(digest.FromString(name).Encoded())
}
func patchErrContextName(err error, name string) error {
if typed, ok := err.(setContextName); ok {
typed.setContext(name)
}
return err
}