From 95e07a2134da1b12a94e632a9cc06b2528012f7f Mon Sep 17 00:00:00 2001 From: Djordje Lukic Date: Tue, 19 May 2020 17:11:31 +0200 Subject: [PATCH 1/3] Add default context to the context ls output --- .github/workflows/ci.yml | 6 +- cli/cmd/context/ls.go | 36 +++++++- cli/cmd/context/ls_test.go | 78 ++++++++++++++++ cli/cmd/context/testdata/ls-out.golden | 3 + cli/cmd/ps.go | 2 +- cli/cmd/ps_test.go | 2 +- cli/cmd/testdata/ps-out.golden | 6 +- context/store/store.go | 120 ++++++++++++++++--------- context/store/store_test.go | 13 +-- context/store/storedefault.go | 75 ++++++++++++++++ context/store/storedefault_test.go | 13 +++ go.mod | 1 + go.sum | 5 ++ 13 files changed, 299 insertions(+), 61 deletions(-) create mode 100644 cli/cmd/context/ls_test.go create mode 100644 cli/cmd/context/testdata/ls-out.golden create mode 100644 context/store/storedefault.go create mode 100644 context/store/storedefault_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34bd70286..106971351 100644 --- a/.github/workflows/ci.yml +++ b/.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 \ No newline at end of file + run: make e2e-local diff --git a/cli/cmd/context/ls.go b/cli/cmd/context/ls.go index 569fce52c..5c7e7ffbc 100644 --- a/cli/cmd/context/ls.go +++ b/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 +} diff --git a/cli/cmd/context/ls_test.go b/cli/cmd/context/ls_test.go new file mode 100644 index 000000000..31bb114b6 --- /dev/null +++ b/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)) +} diff --git a/cli/cmd/context/testdata/ls-out.golden b/cli/cmd/context/testdata/ls-out.golden new file mode 100644 index 000000000..44d5702af --- /dev/null +++ b/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 diff --git a/cli/cmd/ps.go b/cli/cmd/ps.go index 0ea4c749d..1b253ab67 100644 --- a/cli/cmd/ps.go +++ b/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 { diff --git a/cli/cmd/ps_test.go b/cli/cmd/ps_test.go index e078d12cd..8011f6f24 100644 --- a/cli/cmd/ps_test.go +++ b/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" diff --git a/cli/cmd/testdata/ps-out.golden b/cli/cmd/testdata/ps-out.golden index 34dd15f60..ee7ef979c 100644 --- a/cli/cmd/testdata/ps-out.golden +++ b/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 diff --git a/context/store/store.go b/context/store/store.go index 2675ca509..d31bd044e 100644 --- a/context/store/store.go +++ b/context/store/store.go @@ -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 diff --git a/context/store/store_test.go b/context/store/store_test.go index f76275f47..c901afaaf 100644 --- a/context/store/store_test.go +++ b/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) { diff --git a/context/store/storedefault.go b/context/store/storedefault.go new file mode 100644 index 000000000..fb656ae3f --- /dev/null +++ b/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 +} diff --git a/context/store/storedefault_test.go b/context/store/storedefault_test.go new file mode 100644 index 000000000..3a0daf50a --- /dev/null +++ b/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) +} diff --git a/go.mod b/go.mod index 4270c3493..907c7c7c0 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 6f8c8169a..dbdc66cb7 100644 --- a/go.sum +++ b/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= From fe36c4924628467a971368b3b693a7fef4c0c415 Mon Sep 17 00:00:00 2001 From: Djordje Lukic Date: Wed, 20 May 2020 14:32:56 +0200 Subject: [PATCH 2/3] Use alpine as base image Installing docker on buster is a pain, use alpine to install it --- Dockerfile | 9 +++++---- backend/v1/backend.pb.go | 2 +- builder.Makefile | 4 ++-- cli/cmd/context/testdata/ls-out.golden | 6 +++--- cli/cmd/ps_test.go | 4 ++-- cli/v1/cli.pb.go | 2 +- compose/v1/compose.pb.go | 2 +- containers/v1/containers.pb.go | 2 +- context/store/store.go | 2 ++ 9 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3f7f2eb49..257cf673b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax = docker/dockerfile:experimental -ARG GO_VERSION=1.14.2 +ARG GO_VERSION=1.14.3-alpine3.11 FROM golang:${GO_VERSION} AS base ARG TARGET_OS=unknown @@ -7,6 +7,8 @@ ARG TARGET_ARCH=unknown ARG PWD=/api ENV GO111MODULE=on +RUN apk update && apk add docker make + WORKDIR ${PWD} ADD go.* ${PWD} RUN go mod download @@ -18,9 +20,7 @@ ARG TARGET_ARCH=unknown ARG PWD=/api ENV GO111MODULE=on -RUN apt-get update && apt-get install --no-install-recommends -y \ - protobuf-compiler \ - libprotobuf-dev +RUN apk update && apk add protoc make RUN go get github.com/golang/protobuf/protoc-gen-go@v1.4.1 && \ go get golang.org/x/tools/cmd/goimports @@ -52,4 +52,5 @@ FROM scratch AS cross COPY --from=make-cross /api/bin/* . FROM base as test +ENV CGO_ENABLED=0 RUN make -f builder.Makefile test diff --git a/backend/v1/backend.pb.go b/backend/v1/backend.pb.go index b93e99e7c..457e7ed65 100644 --- a/backend/v1/backend.pb.go +++ b/backend/v1/backend.pb.go @@ -27,7 +27,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.22.0 -// protoc v3.6.1 +// protoc v3.11.2 // source: backend/v1/backend.proto package v1 diff --git a/builder.Makefile b/builder.Makefile index 4b2cff499..763106cc5 100644 --- a/builder.Makefile +++ b/builder.Makefile @@ -26,14 +26,14 @@ GOOS ?= $(shell go env GOOS) GOARCH ?= $(shell go env GOARCH) -PROTOS=$(shell find . -name \*.proto) +PROTOS=$(shell find . -not \( -path ./tests -prune \) -name \*.proto) EXTENSION := ifeq ($(GOOS),windows) EXTENSION := .exe endif -STATIC_FLAGS= CGO_ENABLED=0 +STATIC_FLAGS=CGO_ENABLED=0 LDFLAGS := "-s -w" GO_BUILD = $(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) diff --git a/cli/cmd/context/testdata/ls-out.golden b/cli/cmd/context/testdata/ls-out.golden index 44d5702af..c4b2668e8 100644 --- a/cli/cmd/context/testdata/ls-out.golden +++ b/cli/cmd/context/testdata/ls-out.golden @@ -1,3 +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 +NAME TYPE DESCRIPTION DOCKER ENPOINT KUBERNETES ENDPOINT ORCHESTRATOR +default docker Current DOCKER_HOST based configuration unix:///var/run/docker.sock swarm +example * example diff --git a/cli/cmd/ps_test.go b/cli/cmd/ps_test.go index 8011f6f24..ed90bfa99 100644 --- a/cli/cmd/ps_test.go +++ b/cli/cmd/ps_test.go @@ -75,7 +75,7 @@ func (sut *PsSuite) TestPs() { } err := runPs(sut.ctx, opts) - assert.NilError(sut.T(), err) + assert.Nil(sut.T(), err) golden.Assert(sut.T(), sut.getStdOut(), "ps-out.golden") } @@ -86,7 +86,7 @@ func (sut *PsSuite) TestPsQuiet() { } err := runPs(sut.ctx, opts) - assert.NilError(sut.T(), err) + assert.Nil(sut.T(), err) golden.Assert(sut.T(), sut.getStdOut(), "ps-out-quiet.golden") } diff --git a/cli/v1/cli.pb.go b/cli/v1/cli.pb.go index 8a8be6910..768d0b634 100644 --- a/cli/v1/cli.pb.go +++ b/cli/v1/cli.pb.go @@ -27,7 +27,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.22.0 -// protoc v3.6.1 +// protoc v3.11.2 // source: cli/v1/cli.proto package v1 diff --git a/compose/v1/compose.pb.go b/compose/v1/compose.pb.go index f0dda24cc..dd41b8767 100644 --- a/compose/v1/compose.pb.go +++ b/compose/v1/compose.pb.go @@ -27,7 +27,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.22.0 -// protoc v3.6.1 +// protoc v3.11.2 // source: compose/v1/compose.proto package v1 diff --git a/containers/v1/containers.pb.go b/containers/v1/containers.pb.go index d78466281..b71a9507f 100644 --- a/containers/v1/containers.pb.go +++ b/containers/v1/containers.pb.go @@ -27,7 +27,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.22.0 -// protoc v3.6.1 +// protoc v3.11.2 // source: containers/v1/containers.proto package v1 diff --git a/context/store/store.go b/context/store/store.go index d31bd044e..441419f22 100644 --- a/context/store/store.go +++ b/context/store/store.go @@ -264,6 +264,8 @@ func (s *store) List() ([]*Metadata, error) { } } + // The default context is not stored in the store, it is in-memory only + // so we need a special case for it. dockerDefault, err := dockerGefaultContext() if err != nil { return nil, err From 8495500aa28e5330b8008c870fec58bfc12f68cf Mon Sep 17 00:00:00 2001 From: Djordje Lukic Date: Wed, 20 May 2020 15:06:08 +0200 Subject: [PATCH 3/3] Add a CliSuite for cli unit tests Makes writing unit tests for commands quite easier --- Dockerfile | 4 +- cli/cmd/context/ls_test.go | 61 ++--------------------- cli/cmd/ps_test.go | 70 ++++----------------------- context/store/store.go | 10 ++-- context/store/storedefault.go | 2 +- context/store/storedefault_test.go | 2 +- tests/framework/clisuite.go | 77 ++++++++++++++++++++++++++++++ 7 files changed, 96 insertions(+), 130 deletions(-) create mode 100644 tests/framework/clisuite.go diff --git a/Dockerfile b/Dockerfile index 257cf673b..fcdb81bc5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax = docker/dockerfile:experimental -ARG GO_VERSION=1.14.3-alpine3.11 +ARG GO_VERSION=1.14.3-alpine FROM golang:${GO_VERSION} AS base ARG TARGET_OS=unknown @@ -7,7 +7,7 @@ ARG TARGET_ARCH=unknown ARG PWD=/api ENV GO111MODULE=on -RUN apk update && apk add docker make +RUN apk update && apk add -U docker make WORKDIR ${PWD} ADD go.* ${PWD} diff --git a/cli/cmd/context/ls_test.go b/cli/cmd/context/ls_test.go index 31bb114b6..861e9156c 100644 --- a/cli/cmd/context/ls_test.go +++ b/cli/cmd/context/ls_test.go @@ -1,76 +1,23 @@ 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" + "github.com/docker/api/tests/framework" ) 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) + framework.CliSuite } func (sut *ContextSuite) TestLs() { - err := runList(sut.ctx) + err := runList(sut.Context()) require.Nil(sut.T(), err) - golden.Assert(sut.T(), sut.getStdOut(), "ls-out.golden") + golden.Assert(sut.T(), sut.GetStdOut(), "ls-out.golden") } func TestPs(t *testing.T) { diff --git a/cli/cmd/ps_test.go b/cli/cmd/ps_test.go index ed90bfa99..4d7ffd56a 100644 --- a/cli/cmd/ps_test.go +++ b/cli/cmd/ps_test.go @@ -1,72 +1,18 @@ package cmd import ( - "context" - "io/ioutil" - "os" "testing" - "github.com/stretchr/testify/assert" "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" _ "github.com/docker/api/example" + "github.com/docker/api/tests/framework" ) type PsSuite struct { - suite.Suite - ctx context.Context - writer *os.File - reader *os.File - originalStdout *os.File - storeRoot string -} - -func (sut *PsSuite) 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 *PsSuite) getStdOut() string { - err := sut.writer.Close() - require.Nil(sut.T(), err) - - out, _ := ioutil.ReadAll(sut.reader) - - return string(out) -} - -func (sut *PsSuite) AfterTest(suiteName, testName string) { - os.Stdout = sut.originalStdout - err := os.RemoveAll(sut.storeRoot) - require.Nil(sut.T(), err) + framework.CliSuite } func (sut *PsSuite) TestPs() { @@ -74,10 +20,10 @@ func (sut *PsSuite) TestPs() { quiet: false, } - err := runPs(sut.ctx, opts) - assert.Nil(sut.T(), err) + err := runPs(sut.Context(), opts) + require.Nil(sut.T(), err) - golden.Assert(sut.T(), sut.getStdOut(), "ps-out.golden") + golden.Assert(sut.T(), sut.GetStdOut(), "ps-out.golden") } func (sut *PsSuite) TestPsQuiet() { @@ -85,10 +31,10 @@ func (sut *PsSuite) TestPsQuiet() { quiet: true, } - err := runPs(sut.ctx, opts) - assert.Nil(sut.T(), err) + err := runPs(sut.Context(), opts) + require.Nil(sut.T(), err) - golden.Assert(sut.T(), sut.getStdOut(), "ps-out-quiet.golden") + golden.Assert(sut.T(), sut.GetStdOut(), "ps-out-quiet.golden") } func TestPs(t *testing.T) { diff --git a/context/store/store.go b/context/store/store.go index 441419f22..9583d6c3b 100644 --- a/context/store/store.go +++ b/context/store/store.go @@ -146,12 +146,12 @@ func read(meta string, getter func() interface{}) (*Metadata, error) { } var um untypedMetadata - if err := marshalTyped(bytes, &um); err != nil { + if err := json.Unmarshal(bytes, &um); err != nil { return nil, err } var uc untypedContext - if err := marshalTyped(um.Metadata, &uc); err != nil { + if err := json.Unmarshal(um.Metadata, &uc); err != nil { return nil, err } if uc.Type == "" { @@ -178,10 +178,6 @@ func read(meta string, getter func() interface{}) (*Metadata, error) { }, 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{} @@ -266,7 +262,7 @@ func (s *store) List() ([]*Metadata, error) { // The default context is not stored in the store, it is in-memory only // so we need a special case for it. - dockerDefault, err := dockerGefaultContext() + dockerDefault, err := dockerDefaultContext() if err != nil { return nil, err } diff --git a/context/store/storedefault.go b/context/store/storedefault.go index fb656ae3f..61283d3fc 100644 --- a/context/store/storedefault.go +++ b/context/store/storedefault.go @@ -31,7 +31,7 @@ type endpoint struct { DefaultNamespace string } -func dockerGefaultContext() (*Metadata, error) { +func dockerDefaultContext() (*Metadata, error) { cmd := exec.Command("docker", "context", "inspect", "default") var stdout bytes.Buffer cmd.Stdout = &stdout diff --git a/context/store/storedefault_test.go b/context/store/storedefault_test.go index 3a0daf50a..7d25e874f 100644 --- a/context/store/storedefault_test.go +++ b/context/store/storedefault_test.go @@ -7,7 +7,7 @@ import ( ) func TestDefaultContext(t *testing.T) { - s, err := dockerGefaultContext() + s, err := dockerDefaultContext() assert.Nil(t, err) assert.Equal(t, "default", s.Name) } diff --git a/tests/framework/clisuite.go b/tests/framework/clisuite.go new file mode 100644 index 000000000..fafae5041 --- /dev/null +++ b/tests/framework/clisuite.go @@ -0,0 +1,77 @@ +package framework + +import ( + "context" + "io/ioutil" + "os" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + apicontext "github.com/docker/api/context" + "github.com/docker/api/context/store" +) + +// CliSuite is a helper struct that creates a configured context +// and captures the output of a command. it should be used in the +// same way as testify.suite.Suite +type CliSuite struct { + suite.Suite + ctx context.Context + writer *os.File + reader *os.File + OriginalStdout *os.File + storeRoot string +} + +// BeforeTest is called by testify.suite +func (sut *CliSuite) 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 +} + +// Context returns a configured context +func (sut *CliSuite) Context() context.Context { + return sut.ctx +} + +// GetStdOut returns the output of the command +func (sut *CliSuite) GetStdOut() string { + err := sut.writer.Close() + require.Nil(sut.T(), err) + + out, _ := ioutil.ReadAll(sut.reader) + + return string(out) +} + +// AfterTest is called by testify.suite +func (sut *CliSuite) AfterTest(suiteName, testName string) { + os.Stdout = sut.OriginalStdout + err := os.RemoveAll(sut.storeRoot) + require.Nil(sut.T(), err) +}