From 144ee29645ca80a98892f7a06449ec0b2201b115 Mon Sep 17 00:00:00 2001 From: Christopher Crone Date: Mon, 11 May 2020 15:49:14 +0200 Subject: [PATCH 1/6] Fix function description typo Signed-off-by: Christopher Crone --- context/store/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context/store/store.go b/context/store/store.go index 22f8fc656..b20400666 100644 --- a/context/store/store.go +++ b/context/store/store.go @@ -63,7 +63,7 @@ type Store interface { // Get returns the context with name, it returns an error if the context // doesn't exist Get(name string, getter func() interface{}) (*Metadata, error) - // GetType reurns the type of the context (docker, aci etc) + // GetType returns 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. From b55f4b0547d984757f9e04c2fa4cb20231e12a49 Mon Sep 17 00:00:00 2001 From: Christopher Crone Date: Mon, 11 May 2020 17:55:49 +0200 Subject: [PATCH 2/6] Add common error definitions Signed-off-by: Christopher Crone --- errdefs/errors.go | 56 ++++++++++++++++++++++++++++++++++++++++++ errdefs/errors_test.go | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 errdefs/errors.go create mode 100644 errdefs/errors_test.go diff --git a/errdefs/errors.go b/errdefs/errors.go new file mode 100644 index 000000000..ff944d1c1 --- /dev/null +++ b/errdefs/errors.go @@ -0,0 +1,56 @@ +/* + Copyright (c) 2020 Docker Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, + modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH + THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package errdefs + +import ( + "github.com/pkg/errors" +) + +var ( + // ErrNotFound is returned when an object is not found + ErrNotFound = errors.New("not found") + // ErrAlreadyExists is returned when an object already exists + ErrAlreadyExists = errors.New("already exists") + // ErrUnknown is returned when the error type is unmapped + ErrUnknown = errors.New("unknown") +) + +// IsNotFoundError returns true if the unwrapped error is ErrNotFound +func IsNotFoundError(err error) bool { + return errors.Is(err, ErrNotFound) +} + +// IsAlreadyExistsError returns true if the unwrapped error is ErrAlreadyExists +func IsAlreadyExistsError(err error) bool { + return errors.Is(err, ErrAlreadyExists) +} + +// IsUnknownError returns true if the unwrapped error is ErrUnknown +func IsUnknownError(err error) bool { + return errors.Is(err, ErrUnknown) +} diff --git a/errdefs/errors_test.go b/errdefs/errors_test.go new file mode 100644 index 000000000..a2c2e48e8 --- /dev/null +++ b/errdefs/errors_test.go @@ -0,0 +1,49 @@ +/* + Copyright (c) 2020 Docker Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, + modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH + THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +package errdefs + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func TestIsNotFound(t *testing.T) { + err := errors.Wrap(ErrNotFound, `object "name"`) + require.True(t, IsNotFoundError(err)) + + require.False(t, IsNotFoundError(errors.New("another error"))) +} + +func TestIsAlreadyExists(t *testing.T) { + err := errors.Wrap(ErrAlreadyExists, `object "name"`) + require.True(t, IsAlreadyExistsError(err)) + + require.False(t, IsAlreadyExistsError(errors.New("another error"))) +} From 4788dd5b93b11ed5f49e2f07ea4f547ec62163d7 Mon Sep 17 00:00:00 2001 From: Christopher Crone Date: Mon, 11 May 2020 17:56:58 +0200 Subject: [PATCH 3/6] Use common errors in context store Signed-off-by: Christopher Crone --- context/store/store.go | 7 ++++--- context/store/store_test.go | 8 +++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/context/store/store.go b/context/store/store.go index b20400666..ea0dd6ca6 100644 --- a/context/store/store.go +++ b/context/store/store.go @@ -30,13 +30,14 @@ package store import ( "context" "encoding/json" - "fmt" "io/ioutil" "os" "path/filepath" "reflect" + "github.com/docker/api/errdefs" "github.com/opencontainers/go-digest" + "github.com/pkg/errors" ) const ( @@ -118,7 +119,7 @@ func (s *store) Get(name string, getter func() interface{}) (*Metadata, error) { meta := filepath.Join(s.root, contextsDir, metadataDir, contextdirOf(name), metaFile) m, err := read(meta, getter) if os.IsNotExist(err) { - return nil, fmt.Errorf("unknown context %q", name) + return nil, errors.Wrapf(errdefs.ErrNotFound, "context %q", name) } else if err != nil { return nil, err } @@ -186,7 +187,7 @@ 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) { - return fmt.Errorf("context %q already exists", name) + return errors.Wrapf(errdefs.ErrAlreadyExists, "context %q", name) } err := os.Mkdir(metaDir, 0755) diff --git a/context/store/store_test.go b/context/store/store_test.go index c1dee9c65..7c8999fbb 100644 --- a/context/store/store_test.go +++ b/context/store/store_test.go @@ -33,6 +33,7 @@ import ( "os" "testing" + "github.com/docker/api/errdefs" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -62,12 +63,17 @@ func (suite *StoreTestSuite) AfterTest(suiteName, testName string) { func (suite *StoreTestSuite) TestCreate() { err := suite.store.Create("test", TypedContext{}) require.Nil(suite.T(), err) + + err = suite.store.Create("test", TypedContext{}) + require.EqualError(suite.T(), err, `context "test": already exists`) + require.True(suite.T(), errdefs.IsAlreadyExistsError(err)) } func (suite *StoreTestSuite) TestGetUnknown() { meta, err := suite.store.Get("unknown", nil) require.Nil(suite.T(), meta) - require.Error(suite.T(), err) + require.EqualError(suite.T(), err, `context "unknown": not found`) + require.True(suite.T(), errdefs.IsNotFoundError(err)) } func (suite *StoreTestSuite) TestGet() { From 3c43606a204030cbd83b43d9476add4d7c663332 Mon Sep 17 00:00:00 2001 From: Christopher Crone Date: Mon, 11 May 2020 17:57:17 +0200 Subject: [PATCH 4/6] Add remove function to context store Signed-off-by: Christopher Crone --- context/store/store.go | 23 +++++++++++++++++++++-- context/store/store_test.go | 20 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/context/store/store.go b/context/store/store.go index ea0dd6ca6..51ad0ea96 100644 --- a/context/store/store.go +++ b/context/store/store.go @@ -30,6 +30,7 @@ package store import ( "context" "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" @@ -71,6 +72,8 @@ type Store interface { Create(name string, data TypedContext) error // List returns the list of created contexts List() ([]*Metadata, error) + // Remove removes a context by name from the context store + Remove(name string) error } type store struct { @@ -119,7 +122,7 @@ func (s *store) Get(name string, getter func() interface{}) (*Metadata, error) { meta := filepath.Join(s.root, contextsDir, metadataDir, contextdirOf(name), metaFile) m, err := read(meta, getter) if os.IsNotExist(err) { - return nil, errors.Wrapf(errdefs.ErrNotFound, "context %q", name) + return nil, errors.Wrap(errdefs.ErrNotFound, objectName(name)) } else if err != nil { return nil, err } @@ -187,7 +190,7 @@ 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) { - return errors.Wrapf(errdefs.ErrAlreadyExists, "context %q", name) + return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name)) } err := os.Mkdir(metaDir, 0755) @@ -238,10 +241,26 @@ func (s *store) List() ([]*Metadata, error) { return result, nil } +func (s *store) Remove(name string) error { + 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)) + } + if err := os.RemoveAll(dir); err != nil { + return errors.Wrapf(errdefs.ErrUnknown, "unable to remove %s: %s", objectName(name), err) + } + return nil +} + func contextdirOf(name string) string { return digest.FromString(name).Encoded() } +func objectName(name string) string { + return fmt.Sprintf("context %q", name) +} + type dummyContext struct{} // Metadata represents the docker context metadata diff --git a/context/store/store_test.go b/context/store/store_test.go index 7c8999fbb..56917969c 100644 --- a/context/store/store_test.go +++ b/context/store/store_test.go @@ -107,6 +107,26 @@ func (suite *StoreTestSuite) TestList() { require.Equal(suite.T(), contexts[1].Name, "test2") } +func (suite *StoreTestSuite) TestRemoveNotFound() { + err := suite.store.Remove("notfound") + require.EqualError(suite.T(), err, `context "notfound": not found`) + require.True(suite.T(), errdefs.IsNotFoundError(err)) +} + +func (suite *StoreTestSuite) TestRemove() { + err := suite.store.Create("testremove", TypedContext{}) + require.Nil(suite.T(), err) + contexts, err := suite.store.List() + require.Nil(suite.T(), err) + require.Equal(suite.T(), len(contexts), 1) + + 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) +} + func TestExampleTestSuite(t *testing.T) { suite.Run(t, new(StoreTestSuite)) } From 1d3ffc0254f00db5fa5813d00175b76428499604 Mon Sep 17 00:00:00 2001 From: Christopher Crone Date: Mon, 11 May 2020 17:57:34 +0200 Subject: [PATCH 5/6] Add context rm command Signed-off-by: Christopher Crone --- cli/cmd/context.go | 26 ++++++++++++++++++++++++++ go.mod | 1 + go.sum | 4 ++++ 3 files changed, 31 insertions(+) diff --git a/cli/cmd/context.go b/cli/cmd/context.go index f8b32c28e..6207e0698 100644 --- a/cli/cmd/context.go +++ b/cli/cmd/context.go @@ -33,6 +33,7 @@ import ( "os" "text/tabwriter" + "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "github.com/docker/api/context/store" @@ -48,6 +49,7 @@ func ContextCommand() *cobra.Command { cmd.AddCommand( createCommand(), listCommand(), + removeCommand(), ) return cmd @@ -91,6 +93,17 @@ func listCommand() *cobra.Command { return cmd } +func removeCommand() *cobra.Command { + return &cobra.Command{ + Use: "rm", + Aliases: []string{"remove"}, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runRemove(cmd.Context(), args) + }, + } +} + func runCreate(ctx context.Context, opts createOpts, name string, contextType string) error { switch contextType { case "aci": @@ -134,3 +147,16 @@ func runList(ctx context.Context) error { return w.Flush() } + +func runRemove(ctx context.Context, args []string) error { + s := store.ContextStore(ctx) + var errs *multierror.Error + for _, n := range args { + if err := s.Remove(n); err != nil { + errs = multierror.Append(errs, err) + } else { + fmt.Println(n) + } + } + return errs.ErrorOrNil() +} diff --git a/go.mod b/go.mod index 2a9f9cadd..14fc964f4 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/gobwas/ws v1.0.3 github.com/golang/protobuf v1.4.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 + github.com/hashicorp/go-multierror v1.1.0 github.com/mitchellh/go-homedir v1.1.0 github.com/onsi/gomega v1.9.0 github.com/opencontainers/go-digest v1.0.0-rc1 diff --git a/go.sum b/go.sum index 90e97510b..0b6d21ec2 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,10 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= From 5a2a5f5f609e7ec7b835d17a847cbdc67fe0723d Mon Sep 17 00:00:00 2001 From: Christopher Crone Date: Tue, 12 May 2020 11:45:02 +0200 Subject: [PATCH 6/6] Use new context rm command for e2e Signed-off-by: Christopher Crone --- tests/e2e/e2e.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/e2e.go b/tests/e2e/e2e.go index 838f61541..3b7064876 100644 --- a/tests/e2e/e2e.go +++ b/tests/e2e/e2e.go @@ -50,7 +50,7 @@ func main() { NewDockerCommand("context", "create", "test-example", "example").ExecOrDie() //Expect(output).To(ContainSubstring("test-example context acitest created")) }) - defer NewCommand("docker", "context", "rm", "test-example", "-f").ExecOrDie() + defer NewDockerCommand("context", "rm", "test-example").ExecOrDie() It("uses the test context", func() { currentContext := NewCommand("docker", "context", "use", "test-example").ExecOrDie()