Add default context to the context ls output

This commit is contained in:
Djordje Lukic 2020-05-19 17:11:31 +02:00
Родитель eae864ac33
Коммит 95e07a2134
13 изменённых файлов: 299 добавлений и 61 удалений

6
.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

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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
cli/cmd/context/testdata/ls-out.golden поставляемый Normal file
Просмотреть файл

@ -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

Просмотреть файл

@ -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 {

Просмотреть файл

@ -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"

6
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

Просмотреть файл

@ -104,35 +104,31 @@ func New(opts ...Opt) (Store, error) {
if err != nil {
return nil, err
}
root := filepath.Join(home, configDir)
if err := createDirIfNotExist(root); err != nil {
return nil, err
}
s := &store{
root: filepath.Join(home, configDir),
}
if _, err := os.Stat(s.root); os.IsNotExist(err) {
if err = os.Mkdir(s.root, 0755); err != nil {
return nil, err
}
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

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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
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
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=