Bläddra i källkod

Merge pull request #82 from chris-crone/context-use

Add context use command
Chris Crone 5 år sedan
förälder
incheckning
cc23137945
12 ändrade filer med 295 tillägg och 81 borttagningar
  1. 33 2
      cli/cmd/context.go
  2. 96 0
      cli/config/config.go
  3. 23 31
      cli/config/flags.go
  4. 34 0
      cli/config/keys.go
  5. 22 20
      cli/main.go
  6. 40 0
      cli/options/options.go
  7. 5 19
      context/flags.go
  8. 13 1
      context/store/store.go
  9. 7 0
      errdefs/errors.go
  10. 19 5
      errdefs/errors_test.go
  11. 0 1
      go.mod
  12. 3 2
      tests/e2e/e2e.go

+ 33 - 2
cli/cmd/context.go

@@ -36,11 +36,13 @@ import (
 	"github.com/hashicorp/go-multierror"
 	"github.com/spf13/cobra"
 
+	cliconfig "github.com/docker/api/cli/config"
+	cliopts "github.com/docker/api/cli/options"
 	"github.com/docker/api/context/store"
 )
 
 // ContextCommand manages contexts
-func ContextCommand() *cobra.Command {
+func ContextCommand(opts *cliopts.GlobalOpts) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "context",
 		Short: "Manage contexts",
@@ -50,6 +52,7 @@ func ContextCommand() *cobra.Command {
 		createCommand(),
 		listCommand(),
 		removeCommand(),
+		useCommand(opts),
 	)
 
 	return cmd
@@ -84,6 +87,7 @@ func createCommand() *cobra.Command {
 func listCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:     "list",
+		Short:   "List available contexts",
 		Aliases: []string{"ls"},
 		Args:    cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
@@ -95,7 +99,8 @@ func listCommand() *cobra.Command {
 
 func removeCommand() *cobra.Command {
 	return &cobra.Command{
-		Use:     "rm",
+		Use:     "rm CONTEXT [CONTEXT...]",
+		Short:   "Remove one or more contexts",
 		Aliases: []string{"remove"},
 		Args:    cobra.MinimumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
@@ -104,6 +109,17 @@ func removeCommand() *cobra.Command {
 	}
 }
 
+func useCommand(opts *cliopts.GlobalOpts) *cobra.Command {
+	return &cobra.Command{
+		Use:   "use CONTEXT",
+		Short: "Set the default context",
+		Args:  cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runUse(cmd.Context(), opts.Config, args[0])
+		},
+	}
+}
+
 func runCreate(ctx context.Context, opts createOpts, name string, contextType string) error {
 	switch contextType {
 	case "aci":
@@ -160,3 +176,18 @@ func runRemove(ctx context.Context, args []string) error {
 	}
 	return errs.ErrorOrNil()
 }
+
+func runUse(ctx context.Context, configDir string, name string) error {
+	s := store.ContextStore(ctx)
+	// Match behavior of existing CLI
+	if name != store.DefaultContextName {
+		if _, err := s.Get(name, nil); err != nil {
+			return err
+		}
+	}
+	if err := cliconfig.WriteCurrentContext(configDir, name); err != nil {
+		return err
+	}
+	fmt.Println(name)
+	return nil
+}

+ 96 - 0
cli/config/config.go

@@ -0,0 +1,96 @@
+/*
+	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 config
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"github.com/pkg/errors"
+)
+
+// LoadFile loads the docker configuration
+func LoadFile(dir string) (*File, error) {
+	f := &File{}
+	err := loadFile(configFilePath(dir), &f)
+	if err != nil {
+		return nil, err
+	}
+	return f, nil
+}
+
+// WriteCurrentContext writes the selected current context to the Docker
+// configuration file. Note, the validity of the context is not checked.
+func WriteCurrentContext(dir string, name string) error {
+	m := map[string]interface{}{}
+	path := configFilePath(dir)
+	err := loadFile(path, &m)
+	if err != nil {
+		return err
+	}
+	// Match existing CLI behavior
+	if name == "default" {
+		delete(m, currentContextKey)
+	} else {
+		m[currentContextKey] = name
+	}
+	return writeFile(path, m)
+}
+
+func writeFile(path string, content map[string]interface{}) error {
+	d, err := json.MarshalIndent(content, "", "\t")
+	if err != nil {
+		return errors.Wrap(err, "unable to marshal config")
+	}
+	err = ioutil.WriteFile(path, d, 0644)
+	return errors.Wrap(err, "unable to write config file")
+}
+
+func loadFile(path string, dest interface{}) error {
+	data, err := ioutil.ReadFile(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			// Not an error if there is no config, we're just using defaults
+			return nil
+		}
+		return errors.Wrap(err, "unable to read config file")
+	}
+	err = json.Unmarshal(data, dest)
+	return errors.Wrap(err, "unable to unmarshal config")
+}
+
+func configFilePath(dir string) string {
+	return filepath.Join(dir, ConfigFileName)
+}
+
+// File contains the current context from the docker configuration file
+type File struct {
+	CurrentContext string `json:"currentContext,omitempty"`
+}

+ 23 - 31
context/config.go → cli/config/flags.go

@@ -25,44 +25,36 @@
 	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
 
-package context
+package config
 
 import (
-	"encoding/json"
-	"fmt"
 	"os"
 	"path/filepath"
+
+	"github.com/spf13/pflag"
+)
+
+const (
+	// ConfigFileName is the name of config file
+	ConfigFileName = "config.json"
+	// ConfigFileDir is the default folder where the config file is stored
+	ConfigFileDir = ".docker"
+	// ConfigFlagName is the name of the config flag
+	ConfigFlagName = "config"
 )
 
-// LoadConfigFile loads the docker configuration
-func LoadConfigFile(configDir string, configFileName string) (*ConfigFile, error) {
-	filename := filepath.Join(configDir, configFileName)
-	configFile := &ConfigFile{
-		Filename: filename,
-	}
+// ConfigFlags are the global CLI flags
+// nolint stutter
+type ConfigFlags struct {
+	Config string
+}
 
-	if _, err := os.Stat(filename); err == nil {
-		file, err := os.Open(filename)
-		if err != nil {
-			return nil, fmt.Errorf("can't read %s: %w", filename, err)
-		}
-		// nolint errcheck
-		defer file.Close()
-		err = json.NewDecoder(file).Decode(&configFile)
-		if err != nil {
-			err = fmt.Errorf("can't read %s: %w", filename, err)
-		}
-		return configFile, err
-	} else if !os.IsNotExist(err) {
-		// if file is there but we can't stat it for any reason other
-		// than it doesn't exist then stop
-		return nil, fmt.Errorf("can't read %s: %w", filename, err)
-	}
-	return configFile, nil
+// AddConfigFlags adds persistent (global) flags
+func (c *ConfigFlags) AddConfigFlags(flags *pflag.FlagSet) {
+	flags.StringVar(&c.Config, ConfigFlagName, filepath.Join(home(), ConfigFileDir), "Location of the client config files `DIRECTORY`")
 }
 
-// ConfigFile contains the current context from the docker configuration file
-type ConfigFile struct {
-	Filename       string `json:"-"` // Note: for internal use only
-	CurrentContext string `json:"currentContext,omitempty"`
+func home() string {
+	home, _ := os.UserHomeDir()
+	return home
 }

+ 34 - 0
cli/config/keys.go

@@ -0,0 +1,34 @@
+/*
+	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 config
+
+const (
+	// currentContextKey is the key used in the Docker config file to set the
+	// default context
+	currentContextKey = "currentContext"
+)

+ 22 - 20
cli/main.go

@@ -30,13 +30,13 @@ package main
 import (
 	"context"
 	"fmt"
-	"log"
 	"os"
 	"os/exec"
 	"os/signal"
 	"path/filepath"
 	"syscall"
 
+	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 
@@ -48,6 +48,8 @@ import (
 	"github.com/docker/api/cli/cmd"
 	"github.com/docker/api/cli/cmd/compose"
 	"github.com/docker/api/cli/cmd/run"
+	cliconfig "github.com/docker/api/cli/config"
+	cliopts "github.com/docker/api/cli/options"
 	apicontext "github.com/docker/api/context"
 	"github.com/docker/api/context/store"
 )
@@ -56,17 +58,12 @@ var (
 	runningOwnCommand bool
 )
 
-type mainOpts struct {
-	apicontext.Flags
-	debug bool
-}
-
 func init() {
 	// initial hack to get the path of the project's bin dir
 	// into the env of this cli for development
 	path, err := filepath.Abs(filepath.Dir(os.Args[0]))
 	if err != nil {
-		log.Fatal(err)
+		fatal(errors.Wrap(err, "unable to get absolute bin path"))
 	}
 	if err := os.Setenv("PATH", fmt.Sprintf("%s:%s", os.Getenv("PATH"), path)); err != nil {
 		panic(err)
@@ -84,7 +81,7 @@ func isOwnCommand(cmd *cobra.Command) bool {
 }
 
 func main() {
-	var opts mainOpts
+	var opts cliopts.GlobalOpts
 	root := &cobra.Command{
 		Use:           "docker",
 		Long:          "docker for the 2020s",
@@ -103,7 +100,7 @@ func main() {
 	}
 
 	root.AddCommand(
-		cmd.ContextCommand(),
+		cmd.ContextCommand(&opts),
 		cmd.PsCommand(),
 		cmd.ServeCommand(),
 		run.Command(),
@@ -122,21 +119,25 @@ func main() {
 		helpFunc(cmd, args)
 	})
 
-	root.PersistentFlags().BoolVarP(&opts.debug, "debug", "d", false, "enable debug output in the logs")
-	opts.AddFlags(root.PersistentFlags())
+	root.PersistentFlags().BoolVarP(&opts.Debug, "debug", "d", false, "enable debug output in the logs")
+	opts.AddConfigFlags(root.PersistentFlags())
+	opts.AddContextFlags(root.PersistentFlags())
 
 	// populate the opts with the global flags
 	_ = root.PersistentFlags().Parse(os.Args[1:])
-	if opts.debug {
+	if opts.Debug {
 		logrus.SetLevel(logrus.DebugLevel)
 	}
 
 	ctx, cancel := newSigContext()
 	defer cancel()
 
-	config, err := apicontext.LoadConfigFile(opts.Config, "config.json")
+	if opts.Config == "" {
+		fatal(errors.New("config path cannot be empty"))
+	}
+	config, err := cliconfig.LoadFile(opts.Config)
 	if err != nil {
-		logrus.Fatal("unable ot find configuration")
+		fatal(errors.Wrap(err, "unable to find configuration file"))
 	}
 	currentContext := opts.Context
 	if currentContext == "" {
@@ -146,15 +147,11 @@ func main() {
 		currentContext = "default"
 	}
 
-	ctx = apicontext.WithCurrentContext(ctx, currentContext)
-	if err != nil {
-		logrus.Fatal(err)
-	}
-
 	s, err := store.New(store.WithRoot(opts.Config))
 	if err != nil {
-		logrus.Fatal(err)
+		fatal(errors.Wrap(err, "unable to create context store"))
 	}
+	ctx = apicontext.WithCurrentContext(ctx, currentContext)
 	ctx = store.WithContextStore(ctx, s)
 
 	if err = root.ExecuteContext(ctx); err != nil {
@@ -201,3 +198,8 @@ func execMoby(ctx context.Context) {
 		os.Exit(0)
 	}
 }
+
+func fatal(err error) {
+	fmt.Fprint(os.Stderr, err)
+	os.Exit(1)
+}

+ 40 - 0
cli/options/options.go

@@ -0,0 +1,40 @@
+/*
+	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 options
+
+import (
+	apicontext "github.com/docker/api/context"
+	cliconfig "github.com/docker/api/cli/config"
+)
+
+// GlobalOpts contains the global CLI options
+type GlobalOpts struct {
+	apicontext.ContextFlags
+	cliconfig.ConfigFlags
+	Debug bool
+}

+ 5 - 19
context/flags.go

@@ -29,31 +29,17 @@ package context
 
 import (
 	"os"
-	"path/filepath"
 
-	"github.com/mitchellh/go-homedir"
 	"github.com/spf13/pflag"
 )
 
-const (
-	// ConfigFileName is the name of config file
-	ConfigFileName = "config.json"
-	configFileDir  = ".docker"
-)
-
-// Flags are the global cli flags
-type Flags struct {
-	Config  string
+// ContextFlags are the global CLI flags
+// nolint stutter
+type ContextFlags struct {
 	Context string
 }
 
-// AddFlags adds persistent (global) flags
-func (c *Flags) AddFlags(flags *pflag.FlagSet) {
-	flags.StringVar(&c.Config, "config", filepath.Join(home(), configFileDir), "Location of the client config files `DIRECTORY`")
+// AddContextFlags adds persistent (global) flags
+func (c *ContextFlags) AddContextFlags(flags *pflag.FlagSet) {
 	flags.StringVarP(&c.Context, "context", "c", os.Getenv("DOCKER_CONTEXT"), "context")
 }
-
-func home() string {
-	home, _ := homedir.Dir()
-	return home
-}

+ 13 - 1
context/store/store.go

@@ -36,9 +36,15 @@ import (
 	"path/filepath"
 	"reflect"
 
-	"github.com/docker/api/errdefs"
 	"github.com/opencontainers/go-digest"
 	"github.com/pkg/errors"
+
+	"github.com/docker/api/errdefs"
+)
+
+const (
+	// DefaultContextName is an automatically generated local context
+	DefaultContextName = "default"
 )
 
 const (
@@ -187,6 +193,9 @@ func (s *store) GetType(meta *Metadata) string {
 }
 
 func (s *store) Create(name string, data TypedContext) error {
+	if name == DefaultContextName {
+		return errors.Wrap(errdefs.ErrAlreadyExists, objectName(name))
+	}
 	dir := contextdirOf(name)
 	metaDir := filepath.Join(s.root, contextsDir, metadataDir, dir)
 	if _, err := os.Stat(metaDir); !os.IsNotExist(err) {
@@ -242,6 +251,9 @@ func (s *store) List() ([]*Metadata, error) {
 }
 
 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))
 	// Check if directory exists because os.RemoveAll returns nil if it doesn't
 	if _, err := os.Stat(dir); os.IsNotExist(err) {

+ 7 - 0
errdefs/errors.go

@@ -36,6 +36,8 @@ var (
 	ErrNotFound = errors.New("not found")
 	// ErrAlreadyExists is returned when an object already exists
 	ErrAlreadyExists = errors.New("already exists")
+	// ErrForbidden is returned when an operation is not permitted
+	ErrForbidden = errors.New("forbidden")
 	// ErrUnknown is returned when the error type is unmapped
 	ErrUnknown = errors.New("unknown")
 )
@@ -50,6 +52,11 @@ func IsAlreadyExistsError(err error) bool {
 	return errors.Is(err, ErrAlreadyExists)
 }
 
+// IsForbiddenError returns true if the unwrapped error is ErrForbidden
+func IsForbiddenError(err error) bool {
+	return errors.Is(err, ErrForbidden)
+}
+
 // IsUnknownError returns true if the unwrapped error is ErrUnknown
 func IsUnknownError(err error) bool {
 	return errors.Is(err, ErrUnknown)

+ 19 - 5
errdefs/errors_test.go

@@ -31,19 +31,33 @@ import (
 	"testing"
 
 	"github.com/pkg/errors"
-	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestIsNotFound(t *testing.T) {
 	err := errors.Wrap(ErrNotFound, `object "name"`)
-	require.True(t, IsNotFoundError(err))
+	assert.True(t, IsNotFoundError(err))
 
-	require.False(t, IsNotFoundError(errors.New("another error")))
+	assert.False(t, IsNotFoundError(errors.New("another error")))
 }
 
 func TestIsAlreadyExists(t *testing.T) {
 	err := errors.Wrap(ErrAlreadyExists, `object "name"`)
-	require.True(t, IsAlreadyExistsError(err))
+	assert.True(t, IsAlreadyExistsError(err))
 
-	require.False(t, IsAlreadyExistsError(errors.New("another error")))
+	assert.False(t, IsAlreadyExistsError(errors.New("another error")))
+}
+
+func TestIsForbidden(t *testing.T) {
+	err := errors.Wrap(ErrForbidden, `object "name"`)
+	assert.True(t, IsForbiddenError(err))
+
+	assert.False(t, IsForbiddenError(errors.New("another error")))
+}
+
+func TestIsUnknown(t *testing.T) {
+	err := errors.Wrap(ErrUnknown, `object "name"`)
+	assert.True(t, IsUnknownError(err))
+
+	assert.False(t, IsUnknownError(errors.New("another error")))
 }

+ 0 - 1
go.mod

@@ -23,7 +23,6 @@ require (
 	github.com/gorilla/mux v1.7.4 // indirect
 	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/morikuni/aec v1.0.0 // indirect
 	github.com/onsi/gomega v1.9.0
 	github.com/opencontainers/go-digest v1.0.0-rc1

+ 3 - 2
tests/e2e/e2e.go

@@ -20,7 +20,7 @@ func main() {
 	})
 
 	It("should be initialized with default context", func() {
-		NewCommand("docker", "context", "use", "default").ExecOrDie()
+		NewDockerCommand("context", "use", "default").ExecOrDie()
 		output := NewCommand("docker", "context", "ls").ExecOrDie()
 		Expect(output).To(Not(ContainSubstring("test-example")))
 		Expect(output).To(ContainSubstring("default *"))
@@ -52,9 +52,10 @@ func main() {
 		// Expect(output).To(ContainSubstring("test-example context acitest created"))
 	})
 	defer NewDockerCommand("context", "rm", "test-example").ExecOrDie()
+	defer NewDockerCommand("context", "use", "default").ExecOrDie()
 
 	It("uses the test context", func() {
-		currentContext := NewCommand("docker", "context", "use", "test-example").ExecOrDie()
+		currentContext := NewDockerCommand("context", "use", "test-example").ExecOrDie()
 		Expect(currentContext).To(ContainSubstring("test-example"))
 		output := NewCommand("docker", "context", "ls").ExecOrDie()
 		Expect(output).To(ContainSubstring("test-example *"))