store.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. /*
  2. Copyright 2020 Docker, Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package store
  14. import (
  15. "context"
  16. "encoding/json"
  17. "fmt"
  18. "io/ioutil"
  19. "os"
  20. "path/filepath"
  21. "reflect"
  22. "github.com/opencontainers/go-digest"
  23. "github.com/pkg/errors"
  24. "github.com/docker/compose-cli/errdefs"
  25. )
  26. const (
  27. // DefaultContextName is an automatically generated local context
  28. DefaultContextName = "default"
  29. // DefaultContextType is the type for all moby contexts (not associated with cli backend)
  30. DefaultContextType = "moby"
  31. // AwsContextType is the type for aws contexts (currently a CLI plugin, not associated with cli backend)
  32. // to be removed with the cli plugin
  33. AwsContextType = "aws"
  34. // EcsContextType is the endpoint key in the context endpoints for an ECS
  35. // backend
  36. EcsContextType = "ecs"
  37. // AciContextType is the endpoint key in the context endpoints for an ACI
  38. // backend
  39. AciContextType = "aci"
  40. // LocalContextType is the endpoint key in the context endpoints for a new
  41. // local backend
  42. LocalContextType = "local"
  43. // ExampleContextType is the endpoint key in the context endpoints for an
  44. // example backend
  45. ExampleContextType = "example"
  46. )
  47. const (
  48. dockerEndpointKey = "docker"
  49. configDir = ".docker"
  50. contextsDir = "contexts"
  51. metadataDir = "meta"
  52. metaFile = "meta.json"
  53. )
  54. type contextStoreKey struct{}
  55. // WithContextStore adds the store to the context
  56. func WithContextStore(ctx context.Context, store Store) context.Context {
  57. return context.WithValue(ctx, contextStoreKey{}, store)
  58. }
  59. // ContextStore returns the store from the context
  60. func ContextStore(ctx context.Context) Store {
  61. s, _ := ctx.Value(contextStoreKey{}).(Store)
  62. return s
  63. }
  64. // Store is the context store
  65. type Store interface {
  66. // Get returns the context with name, it returns an error if the context
  67. // doesn't exist
  68. Get(name string) (*DockerContext, error)
  69. // GetEndpoint sets the `v` parameter to the value of the endpoint for a
  70. // particular context type
  71. GetEndpoint(name string, v interface{}) error
  72. // Create creates a new context, it returns an error if a context with the
  73. // same name exists already.
  74. Create(name string, contextType string, description string, data interface{}) error
  75. // List returns the list of created contexts
  76. List() ([]*DockerContext, error)
  77. // Remove removes a context by name from the context store
  78. Remove(name string) error
  79. // ContextExists checks if a context already exists
  80. ContextExists(name string) bool
  81. }
  82. // Endpoint holds the Docker or the Kubernetes endpoint, they both have the
  83. // `Host` property, only kubernetes will have the `DefaultNamespace`
  84. type Endpoint struct {
  85. Host string `json:",omitempty"`
  86. DefaultNamespace string `json:",omitempty"`
  87. }
  88. type store struct {
  89. root string
  90. }
  91. // Opt is a functional option for the store
  92. type Opt func(*store)
  93. // WithRoot sets a new root to the store
  94. func WithRoot(root string) Opt {
  95. return func(s *store) {
  96. s.root = root
  97. }
  98. }
  99. // New returns a configured context store with $HOME/.docker as root
  100. func New(opts ...Opt) (Store, error) {
  101. home, err := os.UserHomeDir()
  102. if err != nil {
  103. return nil, err
  104. }
  105. root := filepath.Join(home, configDir)
  106. if err := createDirIfNotExist(root); err != nil {
  107. return nil, err
  108. }
  109. s := &store{
  110. root: root,
  111. }
  112. for _, opt := range opts {
  113. opt(s)
  114. }
  115. m := filepath.Join(s.root, contextsDir, metadataDir)
  116. if err := createDirIfNotExist(m); err != nil {
  117. return nil, err
  118. }
  119. return s, nil
  120. }
  121. // Get returns the context with the given name
  122. func (s *store) Get(name string) (*DockerContext, error) {
  123. if name == "default" {
  124. return dockerDefaultContext()
  125. }
  126. meta := filepath.Join(s.root, contextsDir, metadataDir, contextDirOf(name), metaFile)
  127. m, err := read(meta)
  128. if os.IsNotExist(err) {
  129. return nil, errors.Wrap(errdefs.ErrNotFound, objectName(name))
  130. } else if err != nil {
  131. return nil, err
  132. }
  133. return m, nil
  134. }
  135. func (s *store) GetEndpoint(name string, data interface{}) error {
  136. meta, err := s.Get(name)
  137. if err != nil {
  138. return err
  139. }
  140. contextType := meta.Type()
  141. if _, ok := meta.Endpoints[contextType]; !ok {
  142. return errors.Wrapf(errdefs.ErrNotFound, "endpoint of type %q", contextType)
  143. }
  144. dstPtrValue := reflect.ValueOf(data)
  145. dstValue := reflect.Indirect(dstPtrValue)
  146. val := reflect.ValueOf(meta.Endpoints[contextType])
  147. valIndirect := reflect.Indirect(val)
  148. if dstValue.Type() != valIndirect.Type() {
  149. return errdefs.ErrWrongContextType
  150. }
  151. dstValue.Set(valIndirect)
  152. return nil
  153. }
  154. func read(meta string) (*DockerContext, error) {
  155. bytes, err := ioutil.ReadFile(meta)
  156. if err != nil {
  157. return nil, err
  158. }
  159. var metadata DockerContext
  160. if err := json.Unmarshal(bytes, &metadata); err != nil {
  161. return nil, err
  162. }
  163. metadata.Endpoints, err = toTypedEndpoints(metadata.Endpoints)
  164. if err != nil {
  165. return nil, err
  166. }
  167. return &metadata, nil
  168. }
  169. func toTypedEndpoints(endpoints map[string]interface{}) (map[string]interface{}, error) {
  170. result := map[string]interface{}{}
  171. for k, v := range endpoints {
  172. bytes, err := json.Marshal(v)
  173. if err != nil {
  174. return nil, err
  175. }
  176. typeGetters := getters()
  177. typeGetter, ok := typeGetters[k]
  178. if !ok {
  179. typeGetter = func() interface{} {
  180. return &Endpoint{}
  181. }
  182. }
  183. val := typeGetter()
  184. err = json.Unmarshal(bytes, &val)
  185. if err != nil {
  186. return nil, err
  187. }
  188. result[k] = val
  189. }
  190. return result, nil
  191. }
  192. func (s *store) ContextExists(name string) bool {
  193. if name == DefaultContextName {
  194. return true
  195. }
  196. dir := contextDirOf(name)
  197. metaDir := filepath.Join(s.root, contextsDir, metadataDir, dir)
  198. if _, err := os.Stat(metaDir); !os.IsNotExist(err) {
  199. return true
  200. }
  201. return false
  202. }
  203. func (s *store) Create(name string, contextType string, description string, data interface{}) error {
  204. if s.ContextExists(name) {
  205. return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name))
  206. }
  207. dir := contextDirOf(name)
  208. metaDir := filepath.Join(s.root, contextsDir, metadataDir, dir)
  209. err := os.Mkdir(metaDir, 0755)
  210. if err != nil {
  211. return err
  212. }
  213. meta := DockerContext{
  214. Name: name,
  215. Metadata: ContextMetadata{
  216. Type: contextType,
  217. Description: description,
  218. },
  219. Endpoints: map[string]interface{}{
  220. (dockerEndpointKey): data,
  221. (contextType): data,
  222. },
  223. }
  224. bytes, err := json.Marshal(&meta)
  225. if err != nil {
  226. return err
  227. }
  228. return ioutil.WriteFile(filepath.Join(metaDir, metaFile), bytes, 0644)
  229. }
  230. func (s *store) List() ([]*DockerContext, error) {
  231. root := filepath.Join(s.root, contextsDir, metadataDir)
  232. c, err := ioutil.ReadDir(root)
  233. if err != nil {
  234. return nil, err
  235. }
  236. var result []*DockerContext
  237. for _, fi := range c {
  238. if fi.IsDir() {
  239. meta := filepath.Join(root, fi.Name(), metaFile)
  240. r, err := read(meta)
  241. if err != nil {
  242. return nil, err
  243. }
  244. result = append(result, r)
  245. }
  246. }
  247. // The default context is not stored in the store, it is in-memory only
  248. // so we need a special case for it.
  249. dockerDefault, err := dockerDefaultContext()
  250. if err != nil {
  251. return nil, err
  252. }
  253. result = append(result, dockerDefault)
  254. return result, nil
  255. }
  256. func (s *store) Remove(name string) error {
  257. if name == DefaultContextName {
  258. return errors.Wrap(errdefs.ErrForbidden, objectName(name))
  259. }
  260. dir := filepath.Join(s.root, contextsDir, metadataDir, contextDirOf(name))
  261. // Check if directory exists because os.RemoveAll returns nil if it doesn't
  262. if _, err := os.Stat(dir); os.IsNotExist(err) {
  263. return errors.Wrap(errdefs.ErrNotFound, objectName(name))
  264. }
  265. if err := os.RemoveAll(dir); err != nil {
  266. return errors.Wrapf(errdefs.ErrUnknown, "unable to remove %s: %s", objectName(name), err)
  267. }
  268. return nil
  269. }
  270. func contextDirOf(name string) string {
  271. return digest.FromString(name).Encoded()
  272. }
  273. func objectName(name string) string {
  274. return fmt.Sprintf("context %q", name)
  275. }
  276. func createDirIfNotExist(dir string) error {
  277. if _, err := os.Stat(dir); os.IsNotExist(err) {
  278. if err = os.MkdirAll(dir, 0755); err != nil {
  279. return err
  280. }
  281. }
  282. return nil
  283. }
  284. // Different context types managed by the store.
  285. // TODO(rumpl): we should make this extensible in the future if we want to
  286. // be able to manage other contexts.
  287. func getters() map[string]func() interface{} {
  288. return map[string]func() interface{}{
  289. AciContextType: func() interface{} {
  290. return &AciContext{}
  291. },
  292. EcsContextType: func() interface{} {
  293. return &EcsContext{}
  294. },
  295. LocalContextType: func() interface{} {
  296. return &LocalContext{}
  297. },
  298. ExampleContextType: func() interface{} {
  299. return &ExampleContext{}
  300. },
  301. }
  302. }