فهرست منبع

Add default context to the context ls output

Djordje Lukic 5 سال پیش
والد
کامیت
95e07a2134

+ 3 - 3
.github/workflows/ci.yml

@@ -54,11 +54,11 @@ jobs:
       - name: Install Protoc
         uses: arduino/setup-protoc@master
         with:
-          version: '3.9.1'
+          version: "3.9.1"
 
       - uses: actions/setup-node@v1
         with:
-          node-version: '10.x'
+          node-version: "10.x"
 
       - name: E2E Test
-        run: make e2e-local
+        run: make e2e-local

+ 32 - 4
cli/cmd/context/ls.go

@@ -31,6 +31,8 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"sort"
+	"strings"
 	"text/tabwriter"
 
 	"github.com/spf13/cobra"
@@ -60,17 +62,43 @@ func runList(ctx context.Context) error {
 		return err
 	}
 
-	w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
-	fmt.Fprintln(w, "NAME\tDESCRIPTION\tTYPE")
-	format := "%s\t%s\t%s\n"
+	sort.Slice(contexts, func(i, j int) bool {
+		return strings.Compare(contexts[i].Name, contexts[j].Name) == -1
+	})
+
+	w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
+	fmt.Fprintln(w, "NAME\tTYPE\tDESCRIPTION\tDOCKER ENPOINT\tKUBERNETES ENDPOINT\tORCHESTRATOR")
+	format := "%s\t%s\t%s\t%s\t%s\t%s\n"
 
 	for _, c := range contexts {
 		contextName := c.Name
 		if c.Name == currentContext {
 			contextName += " *"
 		}
-		fmt.Fprintf(w, format, contextName, c.Metadata.Description, c.Metadata.Type)
+
+		fmt.Fprintf(w,
+			format,
+			contextName,
+			c.Metadata.Type,
+			c.Metadata.Description,
+			getEndpoint("docker", c.Endpoints),
+			getEndpoint("kubernetes", c.Endpoints),
+			c.Metadata.StackOrchestrator)
 	}
 
 	return w.Flush()
 }
+
+func getEndpoint(name string, meta map[string]store.Endpoint) string {
+	d, ok := meta[name]
+	if !ok {
+		return ""
+	}
+
+	result := d.Host
+	if d.DefaultNamespace != "" {
+		result += fmt.Sprintf(" (%s)", d.DefaultNamespace)
+	}
+
+	return result
+}

+ 78 - 0
cli/cmd/context/ls_test.go

@@ -0,0 +1,78 @@
+package context
+
+import (
+	"context"
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	"gotest.tools/v3/golden"
+
+	apicontext "github.com/docker/api/context"
+	"github.com/docker/api/context/store"
+)
+
+type ContextSuite struct {
+	suite.Suite
+	ctx            context.Context
+	writer         *os.File
+	reader         *os.File
+	originalStdout *os.File
+	storeRoot      string
+}
+
+func (sut *ContextSuite) BeforeTest(suiteName, testName string) {
+	ctx := context.Background()
+	ctx = apicontext.WithCurrentContext(ctx, "example")
+	dir, err := ioutil.TempDir("", "store")
+	require.Nil(sut.T(), err)
+	s, err := store.New(
+		store.WithRoot(dir),
+	)
+	require.Nil(sut.T(), err)
+
+	err = s.Create("example", store.TypedContext{
+		Type: "example",
+	})
+	require.Nil(sut.T(), err)
+
+	sut.storeRoot = dir
+
+	ctx = store.WithContextStore(ctx, s)
+	sut.ctx = ctx
+
+	sut.originalStdout = os.Stdout
+	r, w, err := os.Pipe()
+	require.Nil(sut.T(), err)
+
+	os.Stdout = w
+	sut.writer = w
+	sut.reader = r
+}
+
+func (sut *ContextSuite) getStdOut() string {
+	err := sut.writer.Close()
+	require.Nil(sut.T(), err)
+
+	out, _ := ioutil.ReadAll(sut.reader)
+
+	return string(out)
+}
+
+func (sut *ContextSuite) AfterTest(suiteName, testName string) {
+	os.Stdout = sut.originalStdout
+	err := os.RemoveAll(sut.storeRoot)
+	require.Nil(sut.T(), err)
+}
+
+func (sut *ContextSuite) TestLs() {
+	err := runList(sut.ctx)
+	require.Nil(sut.T(), err)
+	golden.Assert(sut.T(), sut.getStdOut(), "ls-out.golden")
+}
+
+func TestPs(t *testing.T) {
+	suite.Run(t, new(ContextSuite))
+}

+ 3 - 0
cli/cmd/context/testdata/ls-out.golden

@@ -0,0 +1,3 @@
+NAME                TYPE                DESCRIPTION                               DOCKER ENPOINT                KUBERNETES ENDPOINT               ORCHESTRATOR
+default             docker              Current DOCKER_HOST based configuration   unix:///var/run/docker.sock   https://35.205.93.167 (default)   swarm
+example *           example                                                                                                                       

+ 1 - 1
cli/cmd/ps.go

@@ -54,7 +54,7 @@ func runPs(ctx context.Context, opts psOpts) error {
 		return nil
 	}
 
-	w := tabwriter.NewWriter(os.Stdout, 0, 0, 8, ' ', 0)
+	w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
 	fmt.Fprintf(w, "CONTAINER ID\tIMAGE\tCOMMAND\tSTATUS\tPORTS\n")
 	format := "%s\t%s\t%s\t%s\t%s\n"
 	for _, c := range containers {

+ 1 - 1
cli/cmd/ps_test.go

@@ -6,9 +6,9 @@ import (
 	"os"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
-	"gotest.tools/v3/assert"
 	"gotest.tools/v3/golden"
 
 	apicontext "github.com/docker/api/context"

+ 3 - 3
cli/cmd/testdata/ps-out.golden

@@ -1,3 +1,3 @@
-CONTAINER ID        IMAGE         COMMAND        STATUS        PORTS
-id                  nginx                                      
-1234                alpine                                     
+CONTAINER ID        IMAGE               COMMAND             STATUS              PORTS
+id                  nginx                                                       
+1234                alpine                                                      

+ 76 - 44
context/store/store.go

@@ -104,35 +104,31 @@ func New(opts ...Opt) (Store, error) {
 	if err != nil {
 		return nil, err
 	}
-	s := &store{
-		root: filepath.Join(home, configDir),
+
+	root := filepath.Join(home, configDir)
+	if err := createDirIfNotExist(root); err != nil {
+		return nil, err
 	}
-	if _, err := os.Stat(s.root); os.IsNotExist(err) {
-		if err = os.Mkdir(s.root, 0755); err != nil {
-			return nil, err
-		}
+
+	s := &store{
+		root: root,
 	}
+
 	for _, opt := range opts {
 		opt(s)
 	}
-	cd := filepath.Join(s.root, contextsDir)
-	if _, err := os.Stat(cd); os.IsNotExist(err) {
-		if err = os.Mkdir(cd, 0755); err != nil {
-			return nil, err
-		}
-	}
-	m := filepath.Join(cd, metadataDir)
-	if _, err := os.Stat(m); os.IsNotExist(err) {
-		if err = os.Mkdir(m, 0755); err != nil {
-			return nil, err
-		}
+
+	m := filepath.Join(s.root, contextsDir, metadataDir)
+	if err := createDirIfNotExist(m); err != nil {
+		return nil, err
 	}
+
 	return s, nil
 }
 
 // Get returns the context with the given name
 func (s *store) Get(name string, getter func() interface{}) (*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)
 	if os.IsNotExist(err) {
 		return nil, errors.Wrap(errdefs.ErrNotFound, objectName(name))
@@ -150,31 +146,42 @@ func read(meta string, getter func() interface{}) (*Metadata, error) {
 	}
 
 	var um untypedMetadata
-	if err := json.Unmarshal(bytes, &um); err != nil {
+	if err := marshalTyped(bytes, &um); err != nil {
 		return nil, err
 	}
 
 	var uc untypedContext
-	if err := json.Unmarshal(um.Metadata, &uc); err != nil {
+	if err := marshalTyped(um.Metadata, &uc); err != nil {
 		return nil, err
 	}
+	if uc.Type == "" {
+		uc.Type = "docker"
+	}
 
-	data, err := parse(uc.Data, getter)
-	if err != nil {
-		return nil, err
+	var data interface{}
+	if uc.Data != nil {
+		data, err = parse(uc.Data, getter)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	return &Metadata{
 		Name:      um.Name,
 		Endpoints: um.Endpoints,
 		Metadata: TypedContext{
-			Description: uc.Description,
-			Type:        uc.Type,
-			Data:        data,
+			StackOrchestrator: uc.StackOrchestrator,
+			Description:       uc.Description,
+			Type:              uc.Type,
+			Data:              data,
 		},
 	}, nil
 }
 
+func marshalTyped(in []byte, val interface{}) error {
+	return json.Unmarshal(in, val)
+}
+
 func parse(payload []byte, getter func() interface{}) (interface{}, error) {
 	if getter == nil {
 		var res map[string]interface{}
@@ -183,10 +190,12 @@ func parse(payload []byte, getter func() interface{}) (interface{}, error) {
 		}
 		return res, nil
 	}
+
 	typed := getter()
 	if err := json.Unmarshal(payload, &typed); err != nil {
 		return nil, err
 	}
+
 	return reflect.ValueOf(typed).Elem().Interface(), nil
 }
 
@@ -204,7 +213,7 @@ func (s *store) Create(name string, data TypedContext) error {
 	if name == DefaultContextName {
 		return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name))
 	}
-	dir := contextdirOf(name)
+	dir := contextDirOf(name)
 	metaDir := filepath.Join(s.root, contextsDir, metadataDir, dir)
 	if _, err := os.Stat(metaDir); !os.IsNotExist(err) {
 		return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name))
@@ -222,9 +231,9 @@ func (s *store) Create(name string, data TypedContext) error {
 	meta := Metadata{
 		Name:     name,
 		Metadata: data,
-		Endpoints: map[string]interface{}{
-			(dockerEndpointKey): dummyContext{},
-			(data.Type):         dummyContext{},
+		Endpoints: map[string]Endpoint{
+			(dockerEndpointKey): {},
+			(data.Type):         {},
 		},
 	}
 
@@ -255,6 +264,12 @@ func (s *store) List() ([]*Metadata, error) {
 		}
 	}
 
+	dockerDefault, err := dockerGefaultContext()
+	if err != nil {
+		return nil, err
+	}
+
+	result = append(result, dockerDefault)
 	return result, nil
 }
 
@@ -262,7 +277,7 @@ func (s *store) Remove(name string) error {
 	if name == DefaultContextName {
 		return errors.Wrap(errdefs.ErrForbidden, objectName(name))
 	}
-	dir := filepath.Join(s.root, contextsDir, metadataDir, contextdirOf(name))
+	dir := filepath.Join(s.root, contextsDir, metadataDir, contextDirOf(name))
 	// Check if directory exists because os.RemoveAll returns nil if it doesn't
 	if _, err := os.Stat(dir); os.IsNotExist(err) {
 		return errors.Wrap(errdefs.ErrNotFound, objectName(name))
@@ -273,7 +288,7 @@ func (s *store) Remove(name string) error {
 	return nil
 }
 
-func contextdirOf(name string) string {
+func contextDirOf(name string) string {
 	return digest.FromString(name).Encoded()
 }
 
@@ -281,32 +296,49 @@ func objectName(name string) string {
 	return fmt.Sprintf("context %q", name)
 }
 
+func createDirIfNotExist(dir string) error {
+	if _, err := os.Stat(dir); os.IsNotExist(err) {
+		if err = os.MkdirAll(dir, 0755); err != nil {
+			return err
+		}
+	}
+	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]interface{} `json:",omitempty"`
+	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]interface{} `json:",omitempty"`
+	Name      string              `json:",omitempty"`
+	Metadata  json.RawMessage     `json:",omitempty"`
+	Endpoints map[string]Endpoint `json:",omitempty"`
 }
 
 type untypedContext struct {
-	Data        json.RawMessage `json:",omitempty"`
-	Description string          `json:",omitempty"`
-	Type        string          `json:",omitempty"`
+	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 {
-	Type        string      `json:",omitempty"`
-	Description string      `json:",omitempty"`
-	Data        interface{} `json:",omitempty"`
+	StackOrchestrator string      `json:",omitempty"`
+	Type              string      `json:",omitempty"`
+	Description       string      `json:",omitempty"`
+	Data              interface{} `json:",omitempty"`
 }
 
 // AciContext is the context for ACI

+ 8 - 5
context/store/store_test.go

@@ -103,9 +103,10 @@ func (suite *StoreTestSuite) TestList() {
 	contexts, err := suite.store.List()
 	require.Nil(suite.T(), err)
 
-	require.Equal(suite.T(), len(contexts), 2)
-	require.Equal(suite.T(), contexts[0].Name, "test1")
-	require.Equal(suite.T(), contexts[1].Name, "test2")
+	require.Equal(suite.T(), len(contexts), 3)
+	require.Equal(suite.T(), "test1", contexts[0].Name)
+	require.Equal(suite.T(), "test2", contexts[1].Name)
+	require.Equal(suite.T(), "default", contexts[2].Name)
 }
 
 func (suite *StoreTestSuite) TestRemoveNotFound() {
@@ -119,13 +120,15 @@ func (suite *StoreTestSuite) TestRemove() {
 	require.Nil(suite.T(), err)
 	contexts, err := suite.store.List()
 	require.Nil(suite.T(), err)
-	require.Equal(suite.T(), len(contexts), 1)
+	require.Equal(suite.T(), len(contexts), 2)
 
 	err = suite.store.Remove("testremove")
 	require.Nil(suite.T(), err)
 	contexts, err = suite.store.List()
 	require.Nil(suite.T(), err)
-	require.Equal(suite.T(), len(contexts), 0)
+	// The default context is always here, that's why we
+	// have len(contexts) == 1
+	require.Equal(suite.T(), len(contexts), 1)
 }
 
 func TestExampleTestSuite(t *testing.T) {

+ 75 - 0
context/store/storedefault.go

@@ -0,0 +1,75 @@
+package store
+
+import (
+	"bytes"
+	"encoding/json"
+	"os/exec"
+
+	"github.com/pkg/errors"
+)
+
+// Represents a context as created by the docker cli
+type defaultContext struct {
+	Metadata  TypedContext
+	Endpoints endpoints
+}
+
+// Normally (in docker/cli code), the endpoints are mapped as map[string]interface{}
+// but docker cli contexts always have a "docker" and "kubernetes" key so we
+// create real types for those to no have to juggle around with interfaces.
+type endpoints struct {
+	Docker     endpoint `json:"docker,omitempty"`
+	Kubernetes endpoint `json:"kubernetes,omitempty"`
+}
+
+// Both "docker" and "kubernetes" endpoints in the docker cli created contexts
+// have a "Host", only kubernetes has the "DefaultNamespace", we put both of
+// those here for easier manipulation and to not have to create two distinct
+// structs
+type endpoint struct {
+	Host             string
+	DefaultNamespace string
+}
+
+func dockerGefaultContext() (*Metadata, error) {
+	cmd := exec.Command("docker", "context", "inspect", "default")
+	var stdout bytes.Buffer
+	cmd.Stdout = &stdout
+	err := cmd.Run()
+	if err != nil {
+		return nil, err
+	}
+
+	var ctx []defaultContext
+	err = json.Unmarshal(stdout.Bytes(), &ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(ctx) != 1 {
+		return nil, errors.New("found more than one default context")
+	}
+
+	defaultCtx := ctx[0]
+
+	meta := Metadata{
+		Name: "default",
+		Endpoints: map[string]Endpoint{
+			"docker": {
+				Host: defaultCtx.Endpoints.Docker.Host,
+			},
+			"kubernetes": {
+				Host:             defaultCtx.Endpoints.Kubernetes.Host,
+				DefaultNamespace: defaultCtx.Endpoints.Kubernetes.DefaultNamespace,
+			},
+		},
+		Metadata: TypedContext{
+			Description:       "Current DOCKER_HOST based configuration",
+			Type:              "docker",
+			StackOrchestrator: defaultCtx.Metadata.StackOrchestrator,
+			Data:              defaultCtx.Metadata,
+		},
+	}
+
+	return &meta, nil
+}

+ 13 - 0
context/store/storedefault_test.go

@@ -0,0 +1,13 @@
+package store
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDefaultContext(t *testing.T) {
+	s, err := dockerGefaultContext()
+	assert.Nil(t, err)
+	assert.Equal(t, "default", s.Name)
+}

+ 1 - 0
go.mod

@@ -40,6 +40,7 @@ require (
 	github.com/stretchr/testify v1.5.1
 	golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0
 	golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
+	golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect
 	golang.org/x/text v0.3.2 // indirect
 	google.golang.org/grpc v1.29.1
 	google.golang.org/protobuf v1.21.0

+ 5 - 0
go.sum

@@ -66,6 +66,7 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -217,7 +218,9 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/robpike/filter v0.0.0-20150108201509-2984852a2183 h1:qDhD/wJDGyWrXKLIKmEKpKK/ejaZlguyeEaLZzmrtzo=
 github.com/robpike/filter v0.0.0-20150108201509-2984852a2183/go.mod h1:3dvYi47BCPInRb2ILlNnrXfl++XpwTWLbIxPyJsUvCw=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
@@ -301,6 +304,8 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=