| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- /*
- Copyright 2020 Docker, Inc.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package store
- import (
- "context"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
- "reflect"
- "github.com/opencontainers/go-digest"
- "github.com/pkg/errors"
- "github.com/docker/api/errdefs"
- )
- const (
- // DefaultContextName is an automatically generated local context
- DefaultContextName = "default"
- // DefaultContextType is the type for all moby contexts (not associated with cli backend)
- DefaultContextType = "moby"
- // AciContextType is the endpoint key in the context endpoints for an ACI
- // backend
- AciContextType = "aci"
- // LocalContextType is the endpoint key in the context endpoints for a new
- // local backend
- LocalContextType = "local"
- // ExampleContextType is the endpoint key in the context endpoints for an
- // example backend
- ExampleContextType = "example"
- )
- const (
- dockerEndpointKey = "docker"
- configDir = ".docker"
- contextsDir = "contexts"
- metadataDir = "meta"
- metaFile = "meta.json"
- )
- type contextStoreKey struct{}
- // WithContextStore adds the store to the context
- func WithContextStore(ctx context.Context, store Store) context.Context {
- return context.WithValue(ctx, contextStoreKey{}, store)
- }
- // ContextStore returns the store from the context
- func ContextStore(ctx context.Context) Store {
- s, _ := ctx.Value(contextStoreKey{}).(Store)
- return s
- }
- // Store is the context store
- type Store interface {
- // Get returns the context with name, it returns an error if the context
- // doesn't exist
- Get(name string) (*DockerContext, error)
- // GetEndpoint sets the `v` parameter to the value of the endpoint for a
- // particular context type
- GetEndpoint(name string, v interface{}) error
- // Create creates a new context, it returns an error if a context with the
- // same name exists already.
- Create(name string, contextType string, description string, data interface{}) error
- // List returns the list of created contexts
- List() ([]*DockerContext, error)
- // Remove removes a context by name from the context store
- Remove(name string) error
- }
- // Endpoint holds the Docker or the Kubernetes endpoint, they both have the
- // `Host` property, only kubernetes will have the `DefaultNamespace`
- type Endpoint struct {
- Host string `json:",omitempty"`
- DefaultNamespace string `json:",omitempty"`
- }
- type store struct {
- root string
- }
- // Opt is a functional option for the store
- type Opt func(*store)
- // WithRoot sets a new root to the store
- func WithRoot(root string) Opt {
- return func(s *store) {
- s.root = root
- }
- }
- // New returns a configured context store with $HOME/.docker as root
- func New(opts ...Opt) (Store, error) {
- home, err := os.UserHomeDir()
- if err != nil {
- return nil, err
- }
- root := filepath.Join(home, configDir)
- if err := createDirIfNotExist(root); err != nil {
- return nil, err
- }
- s := &store{
- root: root,
- }
- for _, opt := range opts {
- opt(s)
- }
- m := filepath.Join(s.root, contextsDir, metadataDir)
- if err := createDirIfNotExist(m); err != nil {
- return nil, err
- }
- return s, nil
- }
- // Get returns the context with the given name
- func (s *store) Get(name string) (*DockerContext, error) {
- meta := filepath.Join(s.root, contextsDir, metadataDir, contextDirOf(name), metaFile)
- m, err := read(meta)
- if os.IsNotExist(err) {
- return nil, errors.Wrap(errdefs.ErrNotFound, objectName(name))
- } else if err != nil {
- return nil, err
- }
- return m, nil
- }
- func (s *store) GetEndpoint(name string, data interface{}) error {
- meta, err := s.Get(name)
- if err != nil {
- return err
- }
- contextType := meta.Type()
- if _, ok := meta.Endpoints[contextType]; !ok {
- return errors.Wrapf(errdefs.ErrNotFound, "endpoint of type %q", contextType)
- }
- dstPtrValue := reflect.ValueOf(data)
- dstValue := reflect.Indirect(dstPtrValue)
- val := reflect.ValueOf(meta.Endpoints[contextType])
- valIndirect := reflect.Indirect(val)
- if dstValue.Type() != valIndirect.Type() {
- return errdefs.ErrWrongContextType
- }
- dstValue.Set(valIndirect)
- return nil
- }
- func read(meta string) (*DockerContext, error) {
- bytes, err := ioutil.ReadFile(meta)
- if err != nil {
- return nil, err
- }
- var metadata DockerContext
- if err := json.Unmarshal(bytes, &metadata); err != nil {
- return nil, err
- }
- metadata.Endpoints, err = toTypedEndpoints(metadata.Endpoints)
- if err != nil {
- return nil, err
- }
- return &metadata, nil
- }
- func toTypedEndpoints(endpoints map[string]interface{}) (map[string]interface{}, error) {
- result := map[string]interface{}{}
- for k, v := range endpoints {
- bytes, err := json.Marshal(v)
- if err != nil {
- return nil, err
- }
- typeGetters := getters()
- typeGetter, ok := typeGetters[k]
- if !ok {
- typeGetter = func() interface{} {
- return &Endpoint{}
- }
- }
- val := typeGetter()
- err = json.Unmarshal(bytes, &val)
- if err != nil {
- return nil, err
- }
- result[k] = val
- }
- return result, nil
- }
- func (s *store) Create(name string, contextType string, description string, data interface{}) error {
- if name == DefaultContextName {
- return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name))
- }
- dir := contextDirOf(name)
- metaDir := filepath.Join(s.root, contextsDir, metadataDir, dir)
- if _, err := os.Stat(metaDir); !os.IsNotExist(err) {
- return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name))
- }
- err := os.Mkdir(metaDir, 0755)
- if err != nil {
- return err
- }
- meta := DockerContext{
- Name: name,
- Metadata: ContextMetadata{
- Type: contextType,
- Description: description,
- },
- Endpoints: map[string]interface{}{
- (dockerEndpointKey): data,
- (contextType): data,
- },
- }
- bytes, err := json.Marshal(&meta)
- if err != nil {
- return err
- }
- return ioutil.WriteFile(filepath.Join(metaDir, metaFile), bytes, 0644)
- }
- func (s *store) List() ([]*DockerContext, error) {
- root := filepath.Join(s.root, contextsDir, metadataDir)
- c, err := ioutil.ReadDir(root)
- if err != nil {
- return nil, err
- }
- var result []*DockerContext
- for _, fi := range c {
- if fi.IsDir() {
- meta := filepath.Join(root, fi.Name(), metaFile)
- r, err := read(meta)
- if err != nil {
- return nil, err
- }
- result = append(result, r)
- }
- }
- // The default context is not stored in the store, it is in-memory only
- // so we need a special case for it.
- dockerDefault, err := dockerDefaultContext()
- if err != nil {
- return nil, err
- }
- result = append(result, dockerDefault)
- return result, nil
- }
- func (s *store) Remove(name string) error {
- if name == DefaultContextName {
- return errors.Wrap(errdefs.ErrForbidden, objectName(name))
- }
- dir := filepath.Join(s.root, contextsDir, metadataDir, contextDirOf(name))
- // Check if directory exists because os.RemoveAll returns nil if it doesn't
- if _, err := os.Stat(dir); os.IsNotExist(err) {
- return errors.Wrap(errdefs.ErrNotFound, objectName(name))
- }
- if err := os.RemoveAll(dir); err != nil {
- return errors.Wrapf(errdefs.ErrUnknown, "unable to remove %s: %s", objectName(name), err)
- }
- return nil
- }
- func contextDirOf(name string) string {
- return digest.FromString(name).Encoded()
- }
- func objectName(name string) string {
- return fmt.Sprintf("context %q", name)
- }
- func createDirIfNotExist(dir string) error {
- if _, err := os.Stat(dir); os.IsNotExist(err) {
- if err = os.MkdirAll(dir, 0755); err != nil {
- return err
- }
- }
- return nil
- }
- // Different context types managed by the store.
- // TODO(rumpl): we should make this extensible in the future if we want to
- // be able to manage other contexts.
- func getters() map[string]func() interface{} {
- return map[string]func() interface{}{
- "aci": func() interface{} {
- return &AciContext{}
- },
- "local": func() interface{} {
- return &LocalContext{}
- },
- "example": func() interface{} {
- return &ExampleContext{}
- },
- }
- }
|