Merge pull request #147 from rumpl/feat-better-context

Change the way a context is stored
This commit is contained in:
Djordje Lukic 2020-05-25 01:11:31 -07:00 committed by GitHub
commit 7b83047dc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 199 additions and 159 deletions

View File

@ -1,8 +1,6 @@
**What I did** **What I did**
**Related issue** **Related issue**
<-- If this is a bug fix, make sure your description includes "fixes #xxxx", or <-- If this is a bug fix, make sure your description includes "fixes #xxxx", or "closes #xxxx" -->
"closes #xxxx"
-->
**(not mandatory) A picture of a cute animal, if possible in relation with what you did** **(not mandatory) A picture of a cute animal, if possible in relation with what you did**

View File

@ -36,19 +36,15 @@ func init() {
}) })
} }
func getter() interface{} {
return &store.AciContext{}
}
// New creates a backend that can manage containers // New creates a backend that can manage containers
func New(ctx context.Context) (backend.Service, error) { func New(ctx context.Context) (backend.Service, error) {
currentContext := apicontext.CurrentContext(ctx) currentContext := apicontext.CurrentContext(ctx)
contextStore := store.ContextStore(ctx) contextStore := store.ContextStore(ctx)
metadata, err := contextStore.Get(currentContext, getter)
if err != nil { var aciContext store.AciContext
return nil, errors.Wrap(err, "wrong context type") if err := contextStore.GetEndpoint(currentContext, &aciContext); err != nil {
return nil, err
} }
aciContext, _ := metadata.Metadata.Data.(store.AciContext)
auth, _ := login.NewAuthorizerFromLogin() auth, _ := login.NewAuthorizerFromLogin()
containerGroupsClient := containerinstance.NewContainerGroupsClient(aciContext.SubscriptionID) containerGroupsClient := containerinstance.NewContainerGroupsClient(aciContext.SubscriptionID)

View File

@ -67,9 +67,7 @@ func runCreate(ctx context.Context, opts createOpts, name string, contextType st
return createACIContext(ctx, name, opts) return createACIContext(ctx, name, opts)
default: default:
s := store.ContextStore(ctx) s := store.ContextStore(ctx)
return s.Create(name, store.TypedContext{ // TODO: we need to implement different contexts for known backends
Type: contextType, return s.Create(name, contextType, opts.description, store.ExampleContext{})
Description: opts.description,
})
} }
} }

View File

@ -29,19 +29,27 @@ package context
import ( import (
"context" "context"
"fmt"
"github.com/docker/api/context/store" "github.com/docker/api/context/store"
) )
func createACIContext(ctx context.Context, name string, opts createOpts) error { func createACIContext(ctx context.Context, name string, opts createOpts) error {
s := store.ContextStore(ctx) s := store.ContextStore(ctx)
return s.Create(name, store.TypedContext{
Type: "aci", description := fmt.Sprintf("%s@%s", opts.aciResourceGroup, opts.aciLocation)
Description: opts.description, if opts.description != "" {
Data: store.AciContext{ description = fmt.Sprintf("%s (%s)", opts.description, description)
}
return s.Create(
name,
store.AciContextType,
description,
store.AciContext{
SubscriptionID: opts.aciSubscriptionID, SubscriptionID: opts.aciSubscriptionID,
Location: opts.aciLocation, Location: opts.aciLocation,
ResourceGroup: opts.aciResourceGroup, ResourceGroup: opts.aciResourceGroup,
}, },
}) )
} }

View File

@ -79,7 +79,7 @@ func runList(ctx context.Context) error {
fmt.Fprintf(w, fmt.Fprintf(w,
format, format,
contextName, contextName,
c.Metadata.Type, c.Type,
c.Metadata.Description, c.Metadata.Description,
getEndpoint("docker", c.Endpoints), getEndpoint("docker", c.Endpoints),
getEndpoint("kubernetes", c.Endpoints), getEndpoint("kubernetes", c.Endpoints),
@ -89,15 +89,19 @@ func runList(ctx context.Context) error {
return w.Flush() return w.Flush()
} }
func getEndpoint(name string, meta map[string]store.Endpoint) string { func getEndpoint(name string, meta map[string]interface{}) string {
d, ok := meta[name] endpoints, ok := meta[name]
if !ok {
return ""
}
data, ok := endpoints.(store.Endpoint)
if !ok { if !ok {
return "" return ""
} }
result := d.Host result := data.Host
if d.DefaultNamespace != "" { if data.DefaultNamespace != "" {
result += fmt.Sprintf(" (%s)", d.DefaultNamespace) result += fmt.Sprintf(" (%s)", data.DefaultNamespace)
} }
return result return result

View File

@ -53,7 +53,7 @@ func runShow(ctx context.Context) error {
// Match behavior of existing CLI // Match behavior of existing CLI
if name != store.DefaultContextName { if name != store.DefaultContextName {
s := store.ContextStore(ctx) s := store.ContextStore(ctx)
if _, err := s.Get(name, nil); err != nil { if _, err := s.Get(name); err != nil {
return err return err
} }
} }

View File

@ -52,7 +52,7 @@ func runUse(ctx context.Context, name string) error {
s := store.ContextStore(ctx) s := store.ContextStore(ctx)
// Match behavior of existing CLI // Match behavior of existing CLI
if name != store.DefaultContextName { if name != store.DefaultContextName {
if _, err := s.Get(name, nil); err != nil { if _, err := s.Get(name); err != nil {
return err return err
} }
} }

View File

@ -77,7 +77,7 @@ func (cs *cliServer) Contexts(ctx context.Context, request *cliv1.ContextsReques
for _, c := range contexts { for _, c := range contexts {
result.Contexts = append(result.Contexts, &cliv1.Context{ result.Contexts = append(result.Contexts, &cliv1.Context{
Name: c.Name, Name: c.Name,
ContextType: c.Metadata.Type, ContextType: c.Type,
}) })
} }
return result, nil return result, nil

View File

@ -182,7 +182,7 @@ func execMoby(ctx context.Context) {
currentContext := apicontext.CurrentContext(ctx) currentContext := apicontext.CurrentContext(ctx)
s := store.ContextStore(ctx) s := store.ContextStore(ctx)
_, err := s.Get(currentContext, nil) _, err := s.Get(currentContext)
// Only run original docker command if the current context is not // Only run original docker command if the current context is not
// ours. // ours.
if err != nil { if err != nil {

View File

@ -44,19 +44,18 @@ func New(ctx context.Context) (*Client, error) {
currentContext := apicontext.CurrentContext(ctx) currentContext := apicontext.CurrentContext(ctx)
s := store.ContextStore(ctx) s := store.ContextStore(ctx)
cc, err := s.Get(currentContext, nil) cc, err := s.Get(currentContext)
if err != nil { if err != nil {
return nil, err return nil, err
} }
contextType := s.GetType(cc)
service, err := backend.Get(ctx, contextType) service, err := backend.Get(ctx, cc.Type)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Client{ return &Client{
backendType: contextType, backendType: cc.Type,
bs: service, bs: service,
}, nil }, nil

View File

@ -72,18 +72,66 @@ func ContextStore(ctx context.Context) Store {
type Store interface { type Store interface {
// Get returns the context with name, it returns an error if the context // Get returns the context with name, it returns an error if the context
// doesn't exist // doesn't exist
Get(name string, getter func() interface{}) (*Metadata, error) Get(name string) (*Metadata, error)
// GetType returns the type of the context (docker, aci etc) // GetEndpoint sets the `v` parameter to the value of the endpoint for a
GetType(meta *Metadata) string // particular context type
GetEndpoint(name string, v interface{}) error
// Create creates a new context, it returns an error if a context with the // Create creates a new context, it returns an error if a context with the
// same name exists already. // same name exists already.
Create(name string, data TypedContext) error Create(name string, contextType string, description string, data interface{}) error
// List returns the list of created contexts // List returns the list of created contexts
List() ([]*Metadata, error) List() ([]*Metadata, error)
// Remove removes a context by name from the context store // Remove removes a context by name from the context store
Remove(name string) error 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"`
}
const (
// AciContextType is the endpoint key in the context endpoints for an ACI
// backend
AciContextType = "aci"
// MobyContextType is the endpoint key in the context endpoints for a moby
// backend
MobyContextType = "moby"
// ExampleContextType is the endpoint key in the context endpoints for an
// example backend
ExampleContextType = "example"
)
// Metadata represents the docker context metadata
type Metadata struct {
Name string `json:",omitempty"`
Type string `json:",omitempty"`
Metadata ContextMetadata `json:",omitempty"`
Endpoints map[string]interface{} `json:",omitempty"`
}
// ContextMetadata is represtentation of the data we put in a context
// metadata
type ContextMetadata struct {
Description string `json:",omitempty"`
StackOrchestrator string `json:",omitempty"`
}
// AciContext is the context for the ACI backend
type AciContext struct {
SubscriptionID string `json:",omitempty"`
Location string `json:",omitempty"`
ResourceGroup string `json:",omitempty"`
}
// MobyContext is the context for the moby backend
type MobyContext struct{}
// ExampleContext is the context for the example backend
type ExampleContext struct{}
type store struct { type store struct {
root string root string
} }
@ -127,9 +175,9 @@ func New(opts ...Opt) (Store, error) {
} }
// Get returns the context with the given name // Get returns the context with the given name
func (s *store) Get(name string, getter func() interface{}) (*Metadata, error) { func (s *store) Get(name string) (*Metadata, error) {
meta := filepath.Join(s.root, contextsDir, metadataDir, contextDirOf(name), metaFile) meta := filepath.Join(s.root, contextsDir, metadataDir, contextDirOf(name), metaFile)
m, err := read(meta, getter) m, err := read(meta)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, errors.Wrap(errdefs.ErrNotFound, objectName(name)) return nil, errors.Wrap(errdefs.ErrNotFound, objectName(name))
} else if err != nil { } else if err != nil {
@ -139,73 +187,75 @@ func (s *store) Get(name string, getter func() interface{}) (*Metadata, error) {
return m, nil return m, nil
} }
func read(meta string, getter func() interface{}) (*Metadata, error) { func (s *store) GetEndpoint(name string, data interface{}) error {
meta, err := s.Get(name)
if err != nil {
return err
}
if _, ok := meta.Endpoints[meta.Type]; !ok {
return errors.Wrapf(errdefs.ErrNotFound, "endpoint of type %q", meta.Type)
}
dstPtrValue := reflect.ValueOf(data)
dstValue := reflect.Indirect(dstPtrValue)
val := reflect.ValueOf(meta.Endpoints[meta.Type])
valIndirect := reflect.Indirect(val)
if dstValue.Type() != valIndirect.Type() {
return errdefs.ErrWrongContextType
}
dstValue.Set(valIndirect)
return nil
}
func read(meta string) (*Metadata, error) {
bytes, err := ioutil.ReadFile(meta) bytes, err := ioutil.ReadFile(meta)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var um untypedMetadata var metadata Metadata
if err := json.Unmarshal(bytes, &um); err != nil { if err := json.Unmarshal(bytes, &metadata); err != nil {
return nil, err return nil, err
} }
var uc untypedContext metadata.Endpoints, err = toTypedEndpoints(metadata.Endpoints)
if err := json.Unmarshal(um.Metadata, &uc); err != nil { if err != nil {
return nil, err return nil, err
} }
if uc.Type == "" {
uc.Type = "docker"
}
var data interface{} return &metadata, nil
if uc.Data != nil { }
data, err = parse(uc.Data, getter)
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 { if err != nil {
return nil, err return nil, err
} }
} typeGetters := getters()
if _, ok := typeGetters[k]; !ok {
result[k] = v
continue
}
return &Metadata{ val := typeGetters[k]()
Name: um.Name, err = json.Unmarshal(bytes, &val)
Endpoints: um.Endpoints, if err != nil {
Metadata: TypedContext{
StackOrchestrator: uc.StackOrchestrator,
Description: uc.Description,
Type: uc.Type,
Data: data,
},
}, nil
}
func parse(payload []byte, getter func() interface{}) (interface{}, error) {
if getter == nil {
var res map[string]interface{}
if err := json.Unmarshal(payload, &res); err != nil {
return nil, err return nil, err
} }
return res, nil
result[k] = val
} }
typed := getter() return result, nil
if err := json.Unmarshal(payload, &typed); err != nil {
return nil, err
}
return reflect.ValueOf(typed).Elem().Interface(), nil
} }
func (s *store) GetType(meta *Metadata) string { func (s *store) Create(name string, contextType string, description string, data interface{}) error {
for k := range meta.Endpoints {
if k != dockerEndpointKey {
return k
}
}
return dockerEndpointKey
}
func (s *store) Create(name string, data TypedContext) error {
if name == DefaultContextName { if name == DefaultContextName {
return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name)) return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name))
} }
@ -220,16 +270,15 @@ func (s *store) Create(name string, data TypedContext) error {
return err return err
} }
if data.Data == nil {
data.Data = dummyContext{}
}
meta := Metadata{ meta := Metadata{
Name: name, Name: name,
Metadata: data, Type: contextType,
Endpoints: map[string]Endpoint{ Metadata: ContextMetadata{
(dockerEndpointKey): {}, Description: description,
(data.Type): {}, },
Endpoints: map[string]interface{}{
(dockerEndpointKey): data,
(contextType): data,
}, },
} }
@ -252,7 +301,7 @@ func (s *store) List() ([]*Metadata, error) {
for _, fi := range c { for _, fi := range c {
if fi.IsDir() { if fi.IsDir() {
meta := filepath.Join(root, fi.Name(), metaFile) meta := filepath.Join(root, fi.Name(), metaFile)
r, err := read(meta, nil) r, err := read(meta)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -303,45 +352,19 @@ func createDirIfNotExist(dir string) error {
return nil return nil
} }
type dummyContext struct{} // Different context types managed by the store.
// TODO(rumpl): we should make this extensible in the future if we want to
// Endpoint holds the Docker or the Kubernetes endpoint // be able to manage other contexts.
type Endpoint struct { func getters() map[string]func() interface{} {
Host string `json:",omitempty"` return map[string]func() interface{}{
DefaultNamespace string `json:",omitempty"` "aci": func() interface{} {
} return &AciContext{}
},
// Metadata represents the docker context metadata "moby": func() interface{} {
type Metadata struct { return &MobyContext{}
Name string `json:",omitempty"` },
Metadata TypedContext `json:",omitempty"` "example": func() interface{} {
Endpoints map[string]Endpoint `json:",omitempty"` return &ExampleContext{}
} },
}
type untypedMetadata struct {
Name string `json:",omitempty"`
Metadata json.RawMessage `json:",omitempty"`
Endpoints map[string]Endpoint `json:",omitempty"`
}
type untypedContext struct {
StackOrchestrator string `json:",omitempty"`
Type string `json:",omitempty"`
Description string `json:",omitempty"`
Data json.RawMessage `json:",omitempty"`
}
// TypedContext is a context with a type (moby, aci, etc...)
type TypedContext struct {
StackOrchestrator string `json:",omitempty"`
Type string `json:",omitempty"`
Description string `json:",omitempty"`
Data interface{} `json:",omitempty"`
}
// AciContext is the context for ACI
type AciContext struct {
SubscriptionID string `json:",omitempty"`
Location string `json:",omitempty"`
ResourceGroup string `json:",omitempty"`
} }

View File

@ -33,6 +33,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -62,42 +63,55 @@ func (suite *StoreTestSuite) AfterTest(suiteName, testName string) {
} }
func (suite *StoreTestSuite) TestCreate() { func (suite *StoreTestSuite) TestCreate() {
err := suite.store.Create("test", TypedContext{}) err := suite.store.Create("test", "test", "description", ContextMetadata{})
require.Nil(suite.T(), err) require.Nil(suite.T(), err)
err = suite.store.Create("test", TypedContext{}) err = suite.store.Create("test", "test", "descrsiption", ContextMetadata{})
require.EqualError(suite.T(), err, `context "test": already exists`) require.EqualError(suite.T(), err, `context "test": already exists`)
require.True(suite.T(), errdefs.IsAlreadyExistsError(err)) require.True(suite.T(), errdefs.IsAlreadyExistsError(err))
} }
func (suite *StoreTestSuite) TestGetEndpoint() {
err := suite.store.Create("aci", "aci", "description", AciContext{
Location: "eu",
})
require.Nil(suite.T(), err)
var ctx AciContext
err = suite.store.GetEndpoint("aci", &ctx)
assert.Equal(suite.T(), nil, err)
assert.Equal(suite.T(), "eu", ctx.Location)
var exampleCtx ExampleContext
err = suite.store.GetEndpoint("aci", &exampleCtx)
assert.EqualError(suite.T(), err, "wrong context type")
}
func (suite *StoreTestSuite) TestGetUnknown() { func (suite *StoreTestSuite) TestGetUnknown() {
meta, err := suite.store.Get("unknown", nil) meta, err := suite.store.Get("unknown")
require.Nil(suite.T(), meta) require.Nil(suite.T(), meta)
require.EqualError(suite.T(), err, `context "unknown": not found`) require.EqualError(suite.T(), err, `context "unknown": not found`)
require.True(suite.T(), errdefs.IsNotFoundError(err)) require.True(suite.T(), errdefs.IsNotFoundError(err))
} }
func (suite *StoreTestSuite) TestGet() { func (suite *StoreTestSuite) TestGet() {
err := suite.store.Create("test", TypedContext{ err := suite.store.Create("test", "type", "description", ContextMetadata{})
Type: "type",
Description: "description",
})
require.Nil(suite.T(), err) require.Nil(suite.T(), err)
meta, err := suite.store.Get("test", nil) meta, err := suite.store.Get("test")
require.Nil(suite.T(), err) require.Nil(suite.T(), err)
require.NotNil(suite.T(), meta) require.NotNil(suite.T(), meta)
require.Equal(suite.T(), "test", meta.Name) require.Equal(suite.T(), "test", meta.Name)
require.Equal(suite.T(), "description", meta.Metadata.Description) require.Equal(suite.T(), "description", meta.Metadata.Description)
require.Equal(suite.T(), "type", meta.Metadata.Type) require.Equal(suite.T(), "type", meta.Type)
} }
func (suite *StoreTestSuite) TestList() { func (suite *StoreTestSuite) TestList() {
err := suite.store.Create("test1", TypedContext{}) err := suite.store.Create("test1", "type", "description", ContextMetadata{})
require.Nil(suite.T(), err) require.Nil(suite.T(), err)
err = suite.store.Create("test2", TypedContext{}) err = suite.store.Create("test2", "type", "description", ContextMetadata{})
require.Nil(suite.T(), err) require.Nil(suite.T(), err)
contexts, err := suite.store.List() contexts, err := suite.store.List()
@ -116,7 +130,7 @@ func (suite *StoreTestSuite) TestRemoveNotFound() {
} }
func (suite *StoreTestSuite) TestRemove() { func (suite *StoreTestSuite) TestRemove() {
err := suite.store.Create("testremove", TypedContext{}) err := suite.store.Create("testremove", "type", "description", ContextMetadata{})
require.Nil(suite.T(), err) require.Nil(suite.T(), err)
contexts, err := suite.store.List() contexts, err := suite.store.List()
require.Nil(suite.T(), err) require.Nil(suite.T(), err)

View File

@ -10,7 +10,7 @@ import (
// Represents a context as created by the docker cli // Represents a context as created by the docker cli
type defaultContext struct { type defaultContext struct {
Metadata TypedContext Metadata ContextMetadata
Endpoints endpoints Endpoints endpoints
} }
@ -54,20 +54,19 @@ func dockerDefaultContext() (*Metadata, error) {
meta := Metadata{ meta := Metadata{
Name: "default", Name: "default",
Endpoints: map[string]Endpoint{ Type: "docker",
"docker": { Endpoints: map[string]interface{}{
"docker": Endpoint{
Host: defaultCtx.Endpoints.Docker.Host, Host: defaultCtx.Endpoints.Docker.Host,
}, },
"kubernetes": { "kubernetes": Endpoint{
Host: defaultCtx.Endpoints.Kubernetes.Host, Host: defaultCtx.Endpoints.Kubernetes.Host,
DefaultNamespace: defaultCtx.Endpoints.Kubernetes.DefaultNamespace, DefaultNamespace: defaultCtx.Endpoints.Kubernetes.DefaultNamespace,
}, },
}, },
Metadata: TypedContext{ Metadata: ContextMetadata{
Description: "Current DOCKER_HOST based configuration", Description: "Current DOCKER_HOST based configuration",
Type: "docker",
StackOrchestrator: defaultCtx.Metadata.StackOrchestrator, StackOrchestrator: defaultCtx.Metadata.StackOrchestrator,
Data: defaultCtx.Metadata,
}, },
} }

View File

@ -47,6 +47,9 @@ var (
ErrNotImplemented = errors.New("not implemented") ErrNotImplemented = errors.New("not implemented")
// ErrParsingFailed is returned when a string cannot be parsed // ErrParsingFailed is returned when a string cannot be parsed
ErrParsingFailed = errors.New("parsing failed") ErrParsingFailed = errors.New("parsing failed")
// ErrWrongContextType is returned when the caller tries to get a context
// with the wrong type
ErrWrongContextType = errors.New("wrong context type")
) )
// IsNotFoundError returns true if the unwrapped error is ErrNotFound // IsNotFoundError returns true if the unwrapped error is ErrNotFound

View File

@ -35,9 +35,7 @@ func (sut *CliSuite) BeforeTest(suiteName, testName string) {
) )
require.Nil(sut.T(), err) require.Nil(sut.T(), err)
err = s.Create("example", store.TypedContext{ err = s.Create("example", "example", "", store.ContextMetadata{})
Type: "example",
})
require.Nil(sut.T(), err) require.Nil(sut.T(), err)
sut.storeRoot = dir sut.storeRoot = dir