diff --git a/cli/command/context/cmd.go b/cli/command/context/cmd.go index 1b6898456d..6dce68aeaa 100644 --- a/cli/command/context/cmd.go +++ b/cli/command/context/cmd.go @@ -1,10 +1,6 @@ package context import ( - "errors" - "fmt" - "regexp" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" @@ -30,20 +26,3 @@ func NewContextCommand(dockerCli command.Cli) *cobra.Command { ) return cmd } - -const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$" - -var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern) - -func validateContextName(name string) error { - if name == "" { - return errors.New("context name cannot be empty") - } - if name == "default" { - return errors.New(`"default" is a reserved context name`) - } - if !restrictedNameRegEx.MatchString(name) { - return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern) - } - return nil -} diff --git a/cli/command/context/create.go b/cli/command/context/create.go index 9d5865a70a..9e6fe0295b 100644 --- a/cli/command/context/create.go +++ b/cli/command/context/create.go @@ -137,7 +137,7 @@ func createNewContext(o *CreateOptions, stackOrchestrator command.Orchestrator, } func checkContextNameForCreation(s store.Reader, name string) error { - if err := validateContextName(name); err != nil { + if err := store.ValidateContextName(name); err != nil { return err } if _, err := s.GetMetadata(name); !store.IsErrContextDoesNotExist(err) { diff --git a/cli/command/context/export.go b/cli/command/context/export.go index fd66a5d490..6013071402 100644 --- a/cli/command/context/export.go +++ b/cli/command/context/export.go @@ -77,7 +77,7 @@ func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error { // RunExport exports a Docker context func RunExport(dockerCli command.Cli, opts *ExportOptions) error { - if err := validateContextName(opts.ContextName); err != nil && opts.ContextName != command.DefaultContextName { + if err := store.ValidateContextName(opts.ContextName); err != nil && opts.ContextName != command.DefaultContextName { return err } ctxMeta, err := dockerCli.ContextStore().GetMetadata(opts.ContextName) diff --git a/cli/command/context/update.go b/cli/command/context/update.go index 9165bb3019..3c67fdd207 100644 --- a/cli/command/context/update.go +++ b/cli/command/context/update.go @@ -68,7 +68,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { // RunUpdate updates a Docker context func RunUpdate(cli command.Cli, o *UpdateOptions) error { - if err := validateContextName(o.Name); err != nil { + if err := store.ValidateContextName(o.Name); err != nil { return err } s := cli.ContextStore() diff --git a/cli/command/context/use.go b/cli/command/context/use.go index 97e3a97056..a3998da69d 100644 --- a/cli/command/context/use.go +++ b/cli/command/context/use.go @@ -5,6 +5,7 @@ import ( "os" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/context/store" "github.com/spf13/cobra" ) @@ -23,7 +24,7 @@ func newUseCommand(dockerCli command.Cli) *cobra.Command { // RunUse set the current Docker context func RunUse(dockerCli command.Cli, name string) error { - if err := validateContextName(name); err != nil && name != "default" { + if err := store.ValidateContextName(name); err != nil && name != "default" { return err } if _, err := dockerCli.ContextStore().GetMetadata(name); err != nil && name != "default" { diff --git a/cli/context/store/store.go b/cli/context/store/store.go index 5ad4d7759b..71220e9ac0 100644 --- a/cli/context/store/store.go +++ b/cli/context/store/store.go @@ -13,6 +13,7 @@ import ( "net/http" "path" "path/filepath" + "regexp" "strings" "github.com/docker/docker/errdefs" @@ -20,6 +21,10 @@ import ( "github.com/pkg/errors" ) +const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$" + +var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern) + // Store provides a context store for easily remembering endpoints configuration type Store interface { Reader @@ -184,6 +189,20 @@ func (s *store) GetStorageInfo(contextName string) StorageInfo { } } +// ValidateContextName checks a context name is valid. +func ValidateContextName(name string) error { + if name == "" { + return errors.New("context name cannot be empty") + } + if name == "default" { + return errors.New(`"default" is a reserved context name`) + } + if !restrictedNameRegEx.MatchString(name) { + return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern) + } + return nil +} + // Export exports an existing namespace into an opaque data stream // This stream is actually a tarball containing context metadata and TLS materials, but it does // not map 1:1 the layout of the context store (don't try to restore it manually without calling store.Import) @@ -427,6 +446,9 @@ func parseMetadata(data []byte, name string) (Metadata, error) { if err := json.Unmarshal(data, &meta); err != nil { return meta, err } + if err := ValidateContextName(name); err != nil { + return Metadata{}, err + } meta.Name = name return meta, nil } diff --git a/cli/context/store/storeconfig_test.go b/cli/context/store/storeconfig_test.go index e7384fb071..4d9a3b63fb 100644 --- a/cli/context/store/storeconfig_test.go +++ b/cli/context/store/storeconfig_test.go @@ -45,3 +45,16 @@ func TestValidFilePaths(t *testing.T) { assert.Equal(t, err == nil, expectedValid, "%q should report valid as: %v", p, expectedValid) } } + +func TestValidateContextName(t *testing.T) { + names := map[string]bool{ + "../../invalid/escape": false, + "/invalid/absolute": false, + `\invalid\windows`: false, + "validname": true, + } + for n, expectedValid := range names { + err := ValidateContextName(n) + assert.Equal(t, err == nil, expectedValid, "%q should report valid as: %v", n, expectedValid) + } +}