Merge pull request #27 from rumpl/change-context-store

ACI context creation
This commit is contained in:
Djordje Lukic 2020-04-29 19:11:00 +02:00 committed by GitHub
commit f5bf355d1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 193 additions and 127 deletions

View File

@ -56,6 +56,9 @@ func ContextCommand() *cobra.Command {
type createOpts struct { type createOpts struct {
description string description string
aciLocation string
aciSubscriptionID string
aciResourceGroup string
} }
func createCommand() *cobra.Command { func createCommand() *cobra.Command {
@ -70,6 +73,9 @@ func createCommand() *cobra.Command {
} }
cmd.Flags().StringVar(&opts.description, "description", "", "Description of the context") cmd.Flags().StringVar(&opts.description, "description", "", "Description of the context")
cmd.Flags().StringVar(&opts.aciLocation, "aci-location", "eastus", "Location")
cmd.Flags().StringVar(&opts.aciSubscriptionID, "aci-subscription-id", "", "Location")
cmd.Flags().StringVar(&opts.aciResourceGroup, "aci-resource-group", "", "Resource group")
return cmd return cmd
} }
@ -87,14 +93,27 @@ func listCommand() *cobra.Command {
} }
func runCreate(ctx context.Context, opts createOpts, name string, contextType string) error { func runCreate(ctx context.Context, opts createOpts, name string, contextType string) error {
switch contextType {
case "aci":
return createACIContext(ctx, name, opts)
default:
s := store.ContextStore(ctx) s := store.ContextStore(ctx)
return s.Create(name, store.TypeContext{ return s.Create(name, store.TypedContext{
Type: contextType,
Description: opts.description, Description: opts.description,
}, map[string]interface{}{ })
// If we don't set anything here the main docker cli }
// doesn't know how to read the context any more }
"docker": CliContext{},
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{
SubscriptionID: opts.aciSubscriptionID,
Location: opts.aciLocation,
ResourceGroup: opts.aciResourceGroup,
},
}) })
} }
@ -110,11 +129,7 @@ func runList(ctx context.Context) error {
format := "%s\t%s\t%s\n" format := "%s\t%s\t%s\n"
for _, c := range contexts { for _, c := range contexts {
meta, ok := c.Metadata.(store.TypeContext) fmt.Fprintf(w, format, c.Name, c.Metadata.Description)
if !ok {
return fmt.Errorf("Unable to list contexts, context %q is not valid", c.Name)
}
fmt.Fprintf(w, format, c.Name, meta.Description, meta.Type)
} }
return w.Flush() return w.Flush()

View File

@ -32,7 +32,6 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"os/exec" "os/exec"
"time"
"github.com/docker/api/client" "github.com/docker/api/client"
"github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/ptypes/empty"
@ -77,7 +76,8 @@ func connect(ctx context.Context) (*client.Client, error) {
if err != nil { if err != nil {
return nil, errors.Wrap(err, "no backend address") return nil, errors.Wrap(err, "no backend address")
} }
c, err := client.New("unix://"+address, 500*time.Millisecond)
c, err := client.New(ctx)
if err != nil { if err != nil {
if err != context.DeadlineExceeded { if err != context.DeadlineExceeded {
return nil, errors.Wrap(err, "connect to backend") return nil, errors.Wrap(err, "connect to backend")
@ -89,7 +89,7 @@ func connect(ctx context.Context) (*client.Client, error) {
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return nil, errors.Wrap(err, "start backend") return nil, errors.Wrap(err, "start backend")
} }
cl, e := client.New("unix://"+address, 10*time.Second) cl, e := client.New(ctx)
return cl, e return cl, e
} }
return c, nil return c, nil

View File

@ -113,7 +113,7 @@ func main() {
ctx, cancel := util.NewSigContext() ctx, cancel := util.NewSigContext()
defer cancel() defer cancel()
ctx, err := withCurrentContext(ctx, opts) ctx, err := apicontext.WithCurrentContext(ctx, opts.Config, opts.Context)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -133,45 +133,14 @@ func main() {
} }
} }
type currentContextKey struct{}
func withCurrentContext(ctx context.Context, opts mainOpts) (context.Context, error) {
config, err := apicontext.LoadConfigFile(opts.Config, "config.json")
if err != nil {
return ctx, err
}
currentContext := opts.Context
if currentContext == "" {
currentContext = config.CurrentContext
}
if currentContext == "" {
currentContext = "default"
}
logrus.Debugf("Current context %q", currentContext)
return context.WithValue(ctx, currentContextKey{}, currentContext), nil
}
// CurrentContext returns the current context name
func CurrentContext(ctx context.Context) string {
cc, _ := ctx.Value(currentContextKey{}).(string)
return cc
}
func execMoby(ctx context.Context) { func execMoby(ctx context.Context) {
currentContext := CurrentContext(ctx) currentContext := apicontext.CurrentContext(ctx)
s := store.ContextStore(ctx) s := store.ContextStore(ctx)
cc, err := s.Get(currentContext) _, err := s.Get(currentContext, nil)
if err != nil {
logrus.Fatal(err)
}
// Only run original docker command if the current context is not // Only run original docker command if the current context is not
// ours. // ours.
_, ok := cc.Metadata.(store.TypeContext) if err != nil {
if !ok {
cmd := exec.Command("docker", os.Args[1:]...) cmd := exec.Command("docker", os.Args[1:]...)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout

View File

@ -29,45 +29,46 @@ package client
import ( import (
"context" "context"
"time"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/backoff"
v1 "github.com/docker/api/backend/v1" v1 "github.com/docker/api/backend/v1"
"github.com/docker/api/containers"
apicontext "github.com/docker/api/context"
"github.com/docker/api/context/store"
"github.com/docker/api/example"
) )
// New returns a GRPC client // New returns a GRPC client
func New(address string, timeout time.Duration) (*Client, error) { func New(ctx context.Context) (*Client, error) {
backoffConfig := backoff.DefaultConfig currentContext := apicontext.CurrentContext(ctx)
backoffConfig.MaxDelay = 3 * time.Second s := store.ContextStore(ctx)
backoffConfig.BaseDelay = 10 * time.Millisecond
connParams := grpc.ConnectParams{ cc, err := s.Get(currentContext, nil)
Backoff: backoffConfig,
}
opts := []grpc.DialOption{
grpc.WithInsecure(),
grpc.WithConnectParams(connParams),
grpc.WithBlock(),
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
conn, err := grpc.DialContext(ctx, address, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
contextType := s.GetType(cc)
return &Client{ return &Client{
conn: conn, backendType: contextType,
BackendClient: v1.NewBackendClient(conn),
}, nil }, nil
} }
type Client struct { type Client struct {
conn *grpc.ClientConn conn *grpc.ClientConn
v1.BackendClient v1.BackendClient
backendType string
}
func (c *Client) ContainerService(ctx context.Context) containers.ContainerService {
return example.New()
} }
func (c *Client) Close() error { func (c *Client) Close() error {
if c.conn != nil {
return c.conn.Close() return c.conn.Close()
}
return nil
} }

35
context/context.go Normal file
View File

@ -0,0 +1,35 @@
package context
import (
gocontext "context"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
type currentContextKey struct{}
func WithCurrentContext(ctx gocontext.Context, configName string, contextName string) (context.Context, error) {
config, err := LoadConfigFile(configName, "config.json")
if err != nil {
return ctx, err
}
currentContext := contextName
if currentContext == "" {
currentContext = config.CurrentContext
}
if currentContext == "" {
currentContext = "default"
}
logrus.Debugf("Current context %q", currentContext)
return context.WithValue(ctx, currentContextKey{}, currentContext), nil
}
// CurrentContext returns the current context name
func CurrentContext(ctx context.Context) string {
cc, _ := ctx.Value(currentContextKey{}).(string)
return cc
}

View File

@ -60,10 +60,12 @@ 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) (*Metadata, error) Get(name string, getter func() interface{}) (*Metadata, error)
// GetType reurns the type of the context (docker, aci etc)
GetType(meta *Metadata) string
// 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 interface{}, endpoints map[string]interface{}) error Create(name string, data TypedContext) error
// List returns the list of created contexts // List returns the list of created contexts
List() ([]*Metadata, error) List() ([]*Metadata, error)
} }
@ -86,8 +88,8 @@ func New(opts ...StoreOpt) (Store, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
s := &store { s := &store{
root: home, root: filepath.Join(home, ".docker"),
} }
for _, opt := range opts { for _, opt := range opts {
opt(s) opt(s)
@ -108,42 +110,72 @@ func New(opts ...StoreOpt) (Store, error) {
} }
// Get returns the context with the given name // Get returns the context with the given name
func (s *store) Get(name string) (*Metadata, error) { func (s *store) Get(name string, getter func() interface{}) (*Metadata, error) {
if name == "default" { if name == "default" {
return &Metadata{}, nil return &Metadata{}, nil
} }
meta := filepath.Join(s.root, contextsDir, metadataDir, contextdirOf(name), metaFile) meta := filepath.Join(s.root, contextsDir, metadataDir, contextdirOf(name), metaFile)
return read(meta) return read(meta, getter)
} }
func read(meta string) (*Metadata, error) { func read(meta string, getter func() interface{}) (*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 r untypedContextMetadata var um untypedMetadata
if err := json.Unmarshal(bytes, &r); err != nil { if err := json.Unmarshal(bytes, &um); err != nil {
return nil, err return nil, err
} }
result := &Metadata{ var uc untypedContext
Name: r.Name, if err := json.Unmarshal(um.Metadata, &uc); err != nil {
Endpoints: r.Endpoints,
}
typed := getter()
if err := json.Unmarshal(r.Metadata, typed); err != nil {
return nil, err return nil, err
} }
result.Metadata = reflect.ValueOf(typed).Elem().Interface() data, err := parse(uc.Data, getter)
if err != nil {
return nil, err
}
return result, nil return &Metadata{
Name: um.Name,
Endpoints: um.Endpoints,
Metadata: TypedContext{
Description: uc.Description,
Type: uc.Type,
Data: data,
},
}, nil
} }
func (s *store) Create(name string, data interface{}, endpoints map[string]interface{}) error { 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 res, nil
}
typed := getter()
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 {
for k := range meta.Endpoints {
if k != "docker" {
return k
}
}
return "docker"
}
func (s *store) Create(name string, data TypedContext) error {
dir := contextdirOf(name) dir := contextdirOf(name)
metaDir := filepath.Join(s.root, contextsDir, metadataDir, dir) metaDir := filepath.Join(s.root, contextsDir, metadataDir, dir)
if _, err := os.Stat(metaDir); !os.IsNotExist(err) { if _, err := os.Stat(metaDir); !os.IsNotExist(err) {
@ -155,10 +187,16 @@ func (s *store) Create(name string, data interface{}, endpoints map[string]inter
return err return err
} }
if data.Data == nil {
data.Data = DummyContext{}
}
meta := Metadata{ meta := Metadata{
Name: name, Name: name,
Metadata: data, Metadata: data,
Endpoints: endpoints, Endpoints: map[string]interface{}{
"docker": DummyContext{},
},
} }
bytes, err := json.Marshal(&meta) bytes, err := json.Marshal(&meta)
@ -180,7 +218,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) r, err := read(meta, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -195,23 +233,34 @@ func contextdirOf(name string) string {
return digest.FromString(name).Encoded() return digest.FromString(name).Encoded()
} }
type DummyContext struct{}
type Metadata struct { type Metadata struct {
Name string `json:",omitempty"` Name string `json:",omitempty"`
Metadata interface{} `json:",omitempty"` Metadata TypedContext `json:",omitempty"`
Endpoints map[string]interface{} `json:",omitempty"` Endpoints map[string]interface{} `json:",omitempty"`
} }
type untypedContextMetadata struct { type untypedMetadata struct {
Metadata json.RawMessage `json:"metadata,omitempty"` Name string `json:",omitempty"`
Endpoints map[string]interface{} `json:"endpoints,omitempty"` Metadata json.RawMessage `json:",omitempty"`
Name string `json:"name,omitempty"` Endpoints map[string]interface{} `json:",omitempty"`
} }
type TypeContext struct { type untypedContext struct {
Data json.RawMessage `json:",omitempty"`
Description string `json:",omitempty"`
Type string `json:",omitempty"`
}
type TypedContext struct {
Type string `json:",omitempty"` Type string `json:",omitempty"`
Description string `json:",omitempty"` Description string `json:",omitempty"`
Data interface{} `json:",omitempty"`
} }
func getter() interface{} { type AciContext struct {
return &TypeContext{} SubscriptionID string `json:",omitempty"`
Location string `json:",omitempty"`
ResourceGroup string `json:",omitempty"`
} }

View File

@ -33,7 +33,6 @@ 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"
) )
@ -60,47 +59,45 @@ func (suite *StoreTestSuite) AfterTest(suiteName, testName string) {
} }
func (suite *StoreTestSuite) TestCreate() { func (suite *StoreTestSuite) TestCreate() {
err := suite.store.Create("test", nil, nil) err := suite.store.Create("test", TypedContext{})
assert.Nil(suite.T(), err) require.Nil(suite.T(), err)
} }
func (suite *StoreTestSuite) TestGetUnknown() { func (suite *StoreTestSuite) TestGetUnknown() {
meta, err := suite.store.Get("unknown") meta, err := suite.store.Get("unknown", nil)
assert.Nil(suite.T(), meta) require.Nil(suite.T(), meta)
assert.Error(suite.T(), err) require.Error(suite.T(), err)
} }
func (suite *StoreTestSuite) TestGet() { func (suite *StoreTestSuite) TestGet() {
err := suite.store.Create("test", TypeContext{ err := suite.store.Create("test", TypedContext{
Type: "type", Type: "type",
Description: "description", Description: "description",
}, nil) })
assert.Nil(suite.T(), err) require.Nil(suite.T(), err)
meta, err := suite.store.Get("test") meta, err := suite.store.Get("test", nil)
assert.Nil(suite.T(), err) require.Nil(suite.T(), err)
assert.NotNil(suite.T(), meta) require.NotNil(suite.T(), meta)
assert.Equal(suite.T(), "test", meta.Name) require.Equal(suite.T(), "test", meta.Name)
m, ok := meta.Metadata.(TypeContext) require.Equal(suite.T(), "description", meta.Metadata.Description)
assert.Equal(suite.T(), ok, true) require.Equal(suite.T(), "type", meta.Metadata.Type)
assert.Equal(suite.T(), "description", m.Description)
assert.Equal(suite.T(), "type", m.Type)
} }
func (suite *StoreTestSuite) TestList() { func (suite *StoreTestSuite) TestList() {
err := suite.store.Create("test1", TypeContext{}, nil) err := suite.store.Create("test1", TypedContext{})
assert.Nil(suite.T(), err) require.Nil(suite.T(), err)
err = suite.store.Create("test2", TypeContext{}, nil) err = suite.store.Create("test2", TypedContext{})
assert.Nil(suite.T(), err) require.Nil(suite.T(), err)
contexts, err := suite.store.List() contexts, err := suite.store.List()
assert.Nil(suite.T(), err) require.Nil(suite.T(), err)
require.Equal(suite.T(), len(contexts), 2) require.Equal(suite.T(), len(contexts), 2)
assert.Equal(suite.T(), contexts[0].Name, "test1") require.Equal(suite.T(), contexts[0].Name, "test1")
assert.Equal(suite.T(), contexts[1].Name, "test2") require.Equal(suite.T(), contexts[1].Name, "test2")
} }
func TestExampleTestSuite(t *testing.T) { func TestExampleTestSuite(t *testing.T) {