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**
**Related issue**
<-- If this is a bug fix, make sure your description includes "fixes #xxxx", or
"closes #xxxx"
-->
<-- If this is a bug fix, make sure your description includes "fixes #xxxx", or "closes #xxxx" -->
**(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
func New(ctx context.Context) (backend.Service, error) {
currentContext := apicontext.CurrentContext(ctx)
contextStore := store.ContextStore(ctx)
metadata, err := contextStore.Get(currentContext, getter)
if err != nil {
return nil, errors.Wrap(err, "wrong context type")
var aciContext store.AciContext
if err := contextStore.GetEndpoint(currentContext, &aciContext); err != nil {
return nil, err
}
aciContext, _ := metadata.Metadata.Data.(store.AciContext)
auth, _ := login.NewAuthorizerFromLogin()
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)
default:
s := store.ContextStore(ctx)
return s.Create(name, store.TypedContext{
Type: contextType,
Description: opts.description,
})
// TODO: we need to implement different contexts for known backends
return s.Create(name, contextType, opts.description, store.ExampleContext{})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -182,7 +182,7 @@ func execMoby(ctx context.Context) {
currentContext := apicontext.CurrentContext(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
// ours.
if err != nil {

View File

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

View File

@ -72,18 +72,66 @@ func ContextStore(ctx context.Context) Store {
type Store interface {
// Get returns the context with name, it returns an error if the context
// doesn't exist
Get(name string, getter func() interface{}) (*Metadata, error)
// GetType returns the type of the context (docker, aci etc)
GetType(meta *Metadata) string
Get(name string) (*Metadata, 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, data TypedContext) error
Create(name string, contextType string, description string, data interface{}) error
// List returns the list of created contexts
List() ([]*Metadata, 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"`
}
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 {
root string
}
@ -127,9 +175,9 @@ func New(opts ...Opt) (Store, error) {
}
// 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)
m, err := read(meta, getter)
m, err := read(meta)
if os.IsNotExist(err) {
return nil, errors.Wrap(errdefs.ErrNotFound, objectName(name))
} else if err != nil {
@ -139,73 +187,75 @@ func (s *store) Get(name string, getter func() interface{}) (*Metadata, error) {
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)
if err != nil {
return nil, err
}
var um untypedMetadata
if err := json.Unmarshal(bytes, &um); err != nil {
var metadata Metadata
if err := json.Unmarshal(bytes, &metadata); err != nil {
return nil, err
}
var uc untypedContext
if err := json.Unmarshal(um.Metadata, &uc); err != nil {
metadata.Endpoints, err = toTypedEndpoints(metadata.Endpoints)
if err != nil {
return nil, err
}
if uc.Type == "" {
uc.Type = "docker"
}
var data interface{}
if uc.Data != nil {
data, err = parse(uc.Data, getter)
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()
if _, ok := typeGetters[k]; !ok {
result[k] = v
continue
}
return &Metadata{
Name: um.Name,
Endpoints: um.Endpoints,
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 {
val := typeGetters[k]()
err = json.Unmarshal(bytes, &val)
if err != nil {
return nil, err
}
return res, nil
result[k] = val
}
typed := getter()
if err := json.Unmarshal(payload, &typed); err != nil {
return nil, err
}
return reflect.ValueOf(typed).Elem().Interface(), nil
return result, nil
}
func (s *store) GetType(meta *Metadata) string {
for k := range meta.Endpoints {
if k != dockerEndpointKey {
return k
}
}
return dockerEndpointKey
}
func (s *store) Create(name string, data TypedContext) error {
func (s *store) Create(name string, contextType string, description string, data interface{}) error {
if name == DefaultContextName {
return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name))
}
@ -220,16 +270,15 @@ func (s *store) Create(name string, data TypedContext) error {
return err
}
if data.Data == nil {
data.Data = dummyContext{}
}
meta := Metadata{
Name: name,
Metadata: data,
Endpoints: map[string]Endpoint{
(dockerEndpointKey): {},
(data.Type): {},
Name: name,
Type: contextType,
Metadata: ContextMetadata{
Description: description,
},
Endpoints: map[string]interface{}{
(dockerEndpointKey): data,
(contextType): data,
},
}
@ -252,7 +301,7 @@ func (s *store) List() ([]*Metadata, error) {
for _, fi := range c {
if fi.IsDir() {
meta := filepath.Join(root, fi.Name(), metaFile)
r, err := read(meta, nil)
r, err := read(meta)
if err != nil {
return nil, err
}
@ -303,45 +352,19 @@ func createDirIfNotExist(dir string) error {
return nil
}
type dummyContext struct{}
// Endpoint holds the Docker or the Kubernetes endpoint
type Endpoint struct {
Host string `json:",omitempty"`
DefaultNamespace string `json:",omitempty"`
}
// Metadata represents the docker context metadata
type Metadata struct {
Name string `json:",omitempty"`
Metadata TypedContext `json:",omitempty"`
Endpoints map[string]Endpoint `json:",omitempty"`
}
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"`
// 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{}
},
"moby": func() interface{} {
return &MobyContext{}
},
"example": func() interface{} {
return &ExampleContext{}
},
}
}

View File

@ -33,6 +33,7 @@ import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -62,42 +63,55 @@ func (suite *StoreTestSuite) AfterTest(suiteName, testName string) {
}
func (suite *StoreTestSuite) TestCreate() {
err := suite.store.Create("test", TypedContext{})
err := suite.store.Create("test", "test", "description", ContextMetadata{})
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.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() {
meta, err := suite.store.Get("unknown", nil)
meta, err := suite.store.Get("unknown")
require.Nil(suite.T(), meta)
require.EqualError(suite.T(), err, `context "unknown": not found`)
require.True(suite.T(), errdefs.IsNotFoundError(err))
}
func (suite *StoreTestSuite) TestGet() {
err := suite.store.Create("test", TypedContext{
Type: "type",
Description: "description",
})
err := suite.store.Create("test", "type", "description", ContextMetadata{})
require.Nil(suite.T(), err)
meta, err := suite.store.Get("test", nil)
meta, err := suite.store.Get("test")
require.Nil(suite.T(), err)
require.NotNil(suite.T(), meta)
require.Equal(suite.T(), "test", meta.Name)
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() {
err := suite.store.Create("test1", TypedContext{})
err := suite.store.Create("test1", "type", "description", ContextMetadata{})
require.Nil(suite.T(), err)
err = suite.store.Create("test2", TypedContext{})
err = suite.store.Create("test2", "type", "description", ContextMetadata{})
require.Nil(suite.T(), err)
contexts, err := suite.store.List()
@ -116,7 +130,7 @@ func (suite *StoreTestSuite) TestRemoveNotFound() {
}
func (suite *StoreTestSuite) TestRemove() {
err := suite.store.Create("testremove", TypedContext{})
err := suite.store.Create("testremove", "type", "description", ContextMetadata{})
require.Nil(suite.T(), err)
contexts, err := suite.store.List()
require.Nil(suite.T(), err)

View File

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

View File

@ -47,6 +47,9 @@ var (
ErrNotImplemented = errors.New("not implemented")
// ErrParsingFailed is returned when a string cannot be parsed
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

View File

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