diff --git a/cli/cmd/context.go b/cli/cmd/context.go index eff416e1a..428ecea62 100644 --- a/cli/cmd/context.go +++ b/cli/cmd/context.go @@ -55,7 +55,10 @@ func ContextCommand() *cobra.Command { } type createOpts struct { - description string + description string + aciLocation string + aciSubscriptionID string + aciResourceGroup string } 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.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 } @@ -87,14 +93,27 @@ func listCommand() *cobra.Command { } 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) + return s.Create(name, store.TypedContext{ + Description: opts.description, + }) + } +} + +func createACIContext(ctx context.Context, name string, opts createOpts) error { s := store.ContextStore(ctx) - return s.Create(name, store.TypeContext{ - Type: contextType, + return s.Create(name, store.TypedContext{ + Type: "aci", 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{}, + 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" for _, c := range contexts { - meta, ok := c.Metadata.(store.TypeContext) - 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) + fmt.Fprintf(w, format, c.Name, c.Metadata.Description) } return w.Flush() diff --git a/cli/cmd/example.go b/cli/cmd/example.go index bc3077325..221aa4ca0 100644 --- a/cli/cmd/example.go +++ b/cli/cmd/example.go @@ -32,7 +32,6 @@ import ( "encoding/json" "os" "os/exec" - "time" "github.com/docker/api/client" "github.com/golang/protobuf/ptypes/empty" @@ -77,7 +76,8 @@ func connect(ctx context.Context) (*client.Client, error) { if err != nil { 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 != context.DeadlineExceeded { 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 { 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 c, nil diff --git a/cli/main.go b/cli/main.go index 6549b992c..9f37379f4 100644 --- a/cli/main.go +++ b/cli/main.go @@ -113,7 +113,7 @@ func main() { ctx, cancel := util.NewSigContext() defer cancel() - ctx, err := withCurrentContext(ctx, opts) + ctx, err := apicontext.WithCurrentContext(ctx, opts.Config, opts.Context) if err != nil { 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) { - currentContext := CurrentContext(ctx) + currentContext := apicontext.CurrentContext(ctx) s := store.ContextStore(ctx) - cc, err := s.Get(currentContext) - if err != nil { - logrus.Fatal(err) - } + _, err := s.Get(currentContext, nil) // Only run original docker command if the current context is not // ours. - _, ok := cc.Metadata.(store.TypeContext) - if !ok { + if err != nil { cmd := exec.Command("docker", os.Args[1:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/client/client.go b/client/client.go index eba41695f..604a6ae5a 100644 --- a/client/client.go +++ b/client/client.go @@ -29,45 +29,46 @@ package client import ( "context" - "time" "google.golang.org/grpc" - "google.golang.org/grpc/backoff" 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 -func New(address string, timeout time.Duration) (*Client, error) { - backoffConfig := backoff.DefaultConfig - backoffConfig.MaxDelay = 3 * time.Second - backoffConfig.BaseDelay = 10 * time.Millisecond - connParams := grpc.ConnectParams{ - 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...) +func New(ctx context.Context) (*Client, error) { + currentContext := apicontext.CurrentContext(ctx) + s := store.ContextStore(ctx) + + cc, err := s.Get(currentContext, nil) if err != nil { return nil, err } + contextType := s.GetType(cc) return &Client{ - conn: conn, - BackendClient: v1.NewBackendClient(conn), + backendType: contextType, }, nil } type Client struct { conn *grpc.ClientConn v1.BackendClient + backendType string +} + +func (c *Client) ContainerService(ctx context.Context) containers.ContainerService { + return example.New() + } func (c *Client) Close() error { - return c.conn.Close() + if c.conn != nil { + return c.conn.Close() + } + return nil } diff --git a/context/context.go b/context/context.go new file mode 100644 index 000000000..39a696fdd --- /dev/null +++ b/context/context.go @@ -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 +} diff --git a/context/store/store.go b/context/store/store.go index f09d36cc4..d1118fc0a 100644 --- a/context/store/store.go +++ b/context/store/store.go @@ -60,10 +60,12 @@ 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) (*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 // 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() ([]*Metadata, error) } @@ -86,8 +88,8 @@ func New(opts ...StoreOpt) (Store, error) { if err != nil { return nil, err } - s := &store { - root: home, + s := &store{ + root: filepath.Join(home, ".docker"), } for _, opt := range opts { opt(s) @@ -108,42 +110,72 @@ func New(opts ...StoreOpt) (Store, error) { } // 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" { return &Metadata{}, nil } 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) if err != nil { return nil, err } - var r untypedContextMetadata - if err := json.Unmarshal(bytes, &r); err != nil { + var um untypedMetadata + if err := json.Unmarshal(bytes, &um); err != nil { return nil, err } - result := &Metadata{ - Name: r.Name, - Endpoints: r.Endpoints, - } - - typed := getter() - if err := json.Unmarshal(r.Metadata, typed); err != nil { + var uc untypedContext + if err := json.Unmarshal(um.Metadata, &uc); err != nil { 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) metaDir := filepath.Join(s.root, contextsDir, metadataDir, dir) 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 } + if data.Data == nil { + data.Data = DummyContext{} + } + meta := Metadata{ - Name: name, - Metadata: data, - Endpoints: endpoints, + Name: name, + Metadata: data, + Endpoints: map[string]interface{}{ + "docker": DummyContext{}, + }, } bytes, err := json.Marshal(&meta) @@ -180,7 +218,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) + r, err := read(meta, nil) if err != nil { return nil, err } @@ -195,23 +233,34 @@ func contextdirOf(name string) string { return digest.FromString(name).Encoded() } +type DummyContext struct{} + type Metadata struct { Name string `json:",omitempty"` - Metadata interface{} `json:",omitempty"` + Metadata TypedContext `json:",omitempty"` Endpoints map[string]interface{} `json:",omitempty"` } -type untypedContextMetadata struct { - Metadata json.RawMessage `json:"metadata,omitempty"` - Endpoints map[string]interface{} `json:"endpoints,omitempty"` - Name string `json:"name,omitempty"` +type untypedMetadata struct { + Name string `json:",omitempty"` + Metadata json.RawMessage `json:",omitempty"` + Endpoints map[string]interface{} `json:",omitempty"` } -type TypeContext struct { - Type string `json:",omitempty"` - Description string `json:",omitempty"` +type untypedContext struct { + Data json.RawMessage `json:",omitempty"` + Description string `json:",omitempty"` + Type string `json:",omitempty"` } -func getter() interface{} { - return &TypeContext{} +type TypedContext struct { + Type string `json:",omitempty"` + Description string `json:",omitempty"` + Data interface{} `json:",omitempty"` +} + +type AciContext struct { + SubscriptionID string `json:",omitempty"` + Location string `json:",omitempty"` + ResourceGroup string `json:",omitempty"` } diff --git a/context/store/store_test.go b/context/store/store_test.go index 39685c7b1..308e0556a 100644 --- a/context/store/store_test.go +++ b/context/store/store_test.go @@ -33,7 +33,6 @@ import ( "os" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -60,47 +59,45 @@ func (suite *StoreTestSuite) AfterTest(suiteName, testName string) { } func (suite *StoreTestSuite) TestCreate() { - err := suite.store.Create("test", nil, nil) - assert.Nil(suite.T(), err) + err := suite.store.Create("test", TypedContext{}) + require.Nil(suite.T(), err) } func (suite *StoreTestSuite) TestGetUnknown() { - meta, err := suite.store.Get("unknown") - assert.Nil(suite.T(), meta) - assert.Error(suite.T(), err) + meta, err := suite.store.Get("unknown", nil) + require.Nil(suite.T(), meta) + require.Error(suite.T(), err) } func (suite *StoreTestSuite) TestGet() { - err := suite.store.Create("test", TypeContext{ + err := suite.store.Create("test", TypedContext{ Type: "type", Description: "description", - }, nil) - assert.Nil(suite.T(), err) + }) + require.Nil(suite.T(), err) - meta, err := suite.store.Get("test") - assert.Nil(suite.T(), err) - assert.NotNil(suite.T(), meta) - assert.Equal(suite.T(), "test", meta.Name) + meta, err := suite.store.Get("test", nil) + require.Nil(suite.T(), err) + require.NotNil(suite.T(), meta) + require.Equal(suite.T(), "test", meta.Name) - m, ok := meta.Metadata.(TypeContext) - assert.Equal(suite.T(), ok, true) - assert.Equal(suite.T(), "description", m.Description) - assert.Equal(suite.T(), "type", m.Type) + require.Equal(suite.T(), "description", meta.Metadata.Description) + require.Equal(suite.T(), "type", meta.Metadata.Type) } func (suite *StoreTestSuite) TestList() { - err := suite.store.Create("test1", TypeContext{}, nil) - assert.Nil(suite.T(), err) + err := suite.store.Create("test1", TypedContext{}) + require.Nil(suite.T(), err) - err = suite.store.Create("test2", TypeContext{}, nil) - assert.Nil(suite.T(), err) + err = suite.store.Create("test2", TypedContext{}) + require.Nil(suite.T(), err) contexts, err := suite.store.List() - assert.Nil(suite.T(), err) + require.Nil(suite.T(), err) require.Equal(suite.T(), len(contexts), 2) - assert.Equal(suite.T(), contexts[0].Name, "test1") - assert.Equal(suite.T(), contexts[1].Name, "test2") + require.Equal(suite.T(), contexts[0].Name, "test1") + require.Equal(suite.T(), contexts[1].Name, "test2") } func TestExampleTestSuite(t *testing.T) {