From f9b4bfa59ba954c8e2d1a0f8cd42f0e75e4d46f3 Mon Sep 17 00:00:00 2001 From: unclejack Date: Sat, 16 Aug 2014 13:27:04 +0300 Subject: [PATCH 01/19] make http usage for registry explicit Docker-DCO-1.1-Signed-off-by: Cristian Staretu (github: unclejack) Conflicts: daemon/config.go daemon/daemon.go graph/pull.go graph/push.go graph/tags.go registry/registry.go registry/service.go --- daemon/config.go | 2 + daemon/daemon.go | 2 +- docs/sources/reference/commandline/cli.md | 3 +- graph/pull.go | 4 +- graph/push.go | 4 +- graph/tags.go | 24 ++++++----- registry/registry.go | 49 +++++++++++++++++++++++ registry/service.go | 2 +- 8 files changed, 74 insertions(+), 16 deletions(-) diff --git a/daemon/config.go b/daemon/config.go index 8780294ce1..bae0c8cd29 100644 --- a/daemon/config.go +++ b/daemon/config.go @@ -31,6 +31,7 @@ type Config struct { BridgeIface string BridgeIP string FixedCIDR string + InsecureRegistries []string InterContainerCommunication bool GraphDriver string GraphOptions []string @@ -55,6 +56,7 @@ func (config *Config) InstallFlags() { flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b") flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking") flag.StringVar(&config.FixedCIDR, []string{"-fixed-cidr"}, "", "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)\nthis subnet must be nested in the bridge subnet (which is defined by -b or --bip)") + opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Make these registries use http") flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Enable inter-container communication") flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver") flag.StringVar(&config.ExecDriver, []string{"e", "-exec-driver"}, "native", "Force the Docker runtime to use a specific exec driver") diff --git a/daemon/daemon.go b/daemon/daemon.go index 235788c684..d3c79c871c 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -831,7 +831,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) } log.Debugf("Creating repository list") - repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g, config.Mirrors) + repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g, config.Mirrors, config.InsecureRegistries) if err != nil { return nil, fmt.Errorf("Couldn't create Tag store: %s", err) } diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 86f02b6cf1..94cbb580b4 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -70,7 +70,8 @@ expect an integer, and they can only be specified once. -g, --graph="/var/lib/docker" Path to use as the root of the Docker runtime -H, --host=[] The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd. --icc=true Enable inter-container communication - --ip=0.0.0.0 Default IP address to use when binding container ports + --insecure-registry=[] Make these registries use http + --ip=0.0.0.0 Default IP address to use when binding container ports --ip-forward=true Enable net.ipv4.ip_forward --ip-masq=true Enable IP masquerading for bridge's IP range --iptables=true Enable Docker's addition of iptables rules diff --git a/graph/pull.go b/graph/pull.go index 5d7e84ed72..fe1237de48 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -113,7 +113,9 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { return job.Error(err) } - endpoint, err := registry.NewEndpoint(hostname) + secure := registry.IsSecure(hostname, s.InsecureRegistries) + + endpoint, err := registry.NewEndpoint(hostname, secure) if err != nil { return job.Error(err) } diff --git a/graph/push.go b/graph/push.go index 3511245b30..6b5b3394ae 100644 --- a/graph/push.go +++ b/graph/push.go @@ -214,7 +214,9 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { return job.Error(err) } - endpoint, err := registry.NewEndpoint(hostname) + secure := registry.IsSecure(hostname, s.InsecureRegistries) + + endpoint, err := registry.NewEndpoint(hostname, secure) if err != nil { return job.Error(err) } diff --git a/graph/tags.go b/graph/tags.go index 31c65ced5c..c62aa86451 100644 --- a/graph/tags.go +++ b/graph/tags.go @@ -23,10 +23,11 @@ var ( ) type TagStore struct { - path string - graph *Graph - mirrors []string - Repositories map[string]Repository + path string + graph *Graph + mirrors []string + InsecureRegistries []string + Repositories map[string]Repository sync.Mutex // FIXME: move push/pull-related fields // to a helper type @@ -54,18 +55,19 @@ func (r Repository) Contains(u Repository) bool { return true } -func NewTagStore(path string, graph *Graph, mirrors []string) (*TagStore, error) { +func NewTagStore(path string, graph *Graph, mirrors []string, insecureRegistries []string) (*TagStore, error) { abspath, err := filepath.Abs(path) if err != nil { return nil, err } store := &TagStore{ - path: abspath, - graph: graph, - mirrors: mirrors, - Repositories: make(map[string]Repository), - pullingPool: make(map[string]chan struct{}), - pushingPool: make(map[string]chan struct{}), + path: abspath, + graph: graph, + mirrors: mirrors, + InsecureRegistries: insecureRegistries, + Repositories: make(map[string]Repository), + pullingPool: make(map[string]chan struct{}), + pushingPool: make(map[string]chan struct{}), } // Load the json file if it exists, otherwise create it. if err := store.reload(); os.IsNotExist(err) { diff --git a/registry/registry.go b/registry/registry.go index fd74b7514e..8f4ae6fa01 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -202,6 +202,55 @@ func ResolveRepositoryName(reposName string) (string, string, error) { return hostname, reposName, nil } +// this method expands the registry name as used in the prefix of a repo +// to a full url. if it already is a url, there will be no change. +func ExpandAndVerifyRegistryUrl(hostname string, secure bool) (endpoint string, err error) { + if strings.HasPrefix(hostname, "http:") || strings.HasPrefix(hostname, "https:") { + // if there is no slash after https:// (8 characters) then we have no path in the url + if strings.LastIndex(hostname, "/") < 9 { + // there is no path given. Expand with default path + hostname = hostname + "/v1/" + } + if _, err := pingRegistryEndpoint(hostname); err != nil { + return "", errors.New("Invalid Registry endpoint: " + err.Error()) + } + return hostname, nil + } + + // use HTTPS if secure, otherwise use HTTP + if secure { + endpoint = fmt.Sprintf("https://%s/v1/", hostname) + } else { + endpoint = fmt.Sprintf("http://%s/v1/", hostname) + } + _, err = pingRegistryEndpoint(endpoint) + if err != nil { + //TODO: triggering highland build can be done there without "failing" + err = fmt.Errorf("Invalid registry endpoint '%s': %s ", endpoint, err) + if secure { + err = fmt.Errorf("%s. If this private registry supports only HTTP, please add `--insecure-registry %s` to the daemon's arguments.", err, hostname) + } + return "", err + } + return endpoint, nil +} + +// this method verifies if the provided hostname is part of the list of +// insecure registries and returns false if HTTP should be used +func IsSecure(hostname string, insecureRegistries []string) (secure bool) { + secure = true + for _, h := range insecureRegistries { + if hostname == h { + secure = false + break + } + } + if hostname == IndexServerAddress() { + secure = true + } + return +} + func trustedLocation(req *http.Request) bool { var ( trusteds = []string{"docker.com", "docker.io"} diff --git a/registry/service.go b/registry/service.go index f7b353000e..334e7c2ed6 100644 --- a/registry/service.go +++ b/registry/service.go @@ -40,7 +40,7 @@ func (s *Service) Auth(job *engine.Job) engine.Status { job.GetenvJson("authConfig", authConfig) // TODO: this is only done here because auth and registry need to be merged into one pkg if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() { - endpoint, err := NewEndpoint(addr) + endpoint, err := NewEndpoint(addr, true) if err != nil { return job.Error(err) } From c0598aced053f7a7e06aebd57329348dbc7dfc10 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 19 Aug 2014 11:54:42 -0700 Subject: [PATCH 02/19] Refactor IsSecure change Fix issue with restoring the tag store and setting static configuration from the daemon. i.e. the field on the TagStore struct must be made internal or the json.Unmarshal in restore will overwrite the insecure registries to be an empty struct. Signed-off-by: Michael Crosby Conflicts: graph/pull.go graph/push.go graph/tags.go --- graph/pull.go | 2 +- graph/push.go | 2 +- graph/tags.go | 5 +++-- registry/registry.go | 44 +++++++++++++++++++------------------------- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/graph/pull.go b/graph/pull.go index fe1237de48..897e0813a1 100644 --- a/graph/pull.go +++ b/graph/pull.go @@ -113,7 +113,7 @@ func (s *TagStore) CmdPull(job *engine.Job) engine.Status { return job.Error(err) } - secure := registry.IsSecure(hostname, s.InsecureRegistries) + secure := registry.IsSecure(hostname, s.insecureRegistries) endpoint, err := registry.NewEndpoint(hostname, secure) if err != nil { diff --git a/graph/push.go b/graph/push.go index 6b5b3394ae..1d2bf72422 100644 --- a/graph/push.go +++ b/graph/push.go @@ -214,7 +214,7 @@ func (s *TagStore) CmdPush(job *engine.Job) engine.Status { return job.Error(err) } - secure := registry.IsSecure(hostname, s.InsecureRegistries) + secure := registry.IsSecure(hostname, s.insecureRegistries) endpoint, err := registry.NewEndpoint(hostname, secure) if err != nil { diff --git a/graph/tags.go b/graph/tags.go index c62aa86451..622d620941 100644 --- a/graph/tags.go +++ b/graph/tags.go @@ -26,7 +26,7 @@ type TagStore struct { path string graph *Graph mirrors []string - InsecureRegistries []string + insecureRegistries []string Repositories map[string]Repository sync.Mutex // FIXME: move push/pull-related fields @@ -60,11 +60,12 @@ func NewTagStore(path string, graph *Graph, mirrors []string, insecureRegistries if err != nil { return nil, err } + store := &TagStore{ path: abspath, graph: graph, mirrors: mirrors, - InsecureRegistries: insecureRegistries, + insecureRegistries: insecureRegistries, Repositories: make(map[string]Repository), pullingPool: make(map[string]chan struct{}), pushingPool: make(map[string]chan struct{}), diff --git a/registry/registry.go b/registry/registry.go index 8f4ae6fa01..bcbce40198 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -204,51 +204,45 @@ func ResolveRepositoryName(reposName string) (string, string, error) { // this method expands the registry name as used in the prefix of a repo // to a full url. if it already is a url, there will be no change. -func ExpandAndVerifyRegistryUrl(hostname string, secure bool) (endpoint string, err error) { - if strings.HasPrefix(hostname, "http:") || strings.HasPrefix(hostname, "https:") { - // if there is no slash after https:// (8 characters) then we have no path in the url - if strings.LastIndex(hostname, "/") < 9 { - // there is no path given. Expand with default path - hostname = hostname + "/v1/" - } - if _, err := pingRegistryEndpoint(hostname); err != nil { - return "", errors.New("Invalid Registry endpoint: " + err.Error()) - } +func ExpandAndVerifyRegistryUrl(hostname string, secure bool) (string, error) { + if hostname == IndexServerAddress() { return hostname, nil } - // use HTTPS if secure, otherwise use HTTP + endpoint := fmt.Sprintf("http://%s/v1/", hostname) + if secure { endpoint = fmt.Sprintf("https://%s/v1/", hostname) - } else { - endpoint = fmt.Sprintf("http://%s/v1/", hostname) } - _, err = pingRegistryEndpoint(endpoint) - if err != nil { + + if _, oerr := pingRegistryEndpoint(endpoint); oerr != nil { //TODO: triggering highland build can be done there without "failing" - err = fmt.Errorf("Invalid registry endpoint '%s': %s ", endpoint, err) + err := fmt.Errorf("Invalid registry endpoint '%s': %s ", endpoint, oerr) + if secure { - err = fmt.Errorf("%s. If this private registry supports only HTTP, please add `--insecure-registry %s` to the daemon's arguments.", err, hostname) + err = fmt.Errorf("%s. If this private registry supports only HTTP, please add `--insecure-registry %s` to the daemon's arguments.", oerr, hostname) } + return "", err } + return endpoint, nil } // this method verifies if the provided hostname is part of the list of // insecure registries and returns false if HTTP should be used -func IsSecure(hostname string, insecureRegistries []string) (secure bool) { - secure = true +func IsSecure(hostname string, insecureRegistries []string) bool { + if hostname == IndexServerAddress() { + return true + } + for _, h := range insecureRegistries { if hostname == h { - secure = false - break + return false } } - if hostname == IndexServerAddress() { - secure = true - } - return + + return true } func trustedLocation(req *http.Request) bool { From c66196a9dc0cd7d19eb3535c52fdbccfa2ee628e Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 19 Aug 2014 12:27:23 -0700 Subject: [PATCH 03/19] Expand documentation for --insecure-registries Signed-off-by: Michael Crosby --- docs/sources/reference/commandline/cli.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 94cbb580b4..ddfc983d3a 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -71,7 +71,7 @@ expect an integer, and they can only be specified once. -H, --host=[] The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd. --icc=true Enable inter-container communication --insecure-registry=[] Make these registries use http - --ip=0.0.0.0 Default IP address to use when binding container ports + --ip=0.0.0.0 Default IP address to use when binding container ports --ip-forward=true Enable net.ipv4.ip_forward --ip-masq=true Enable IP masquerading for bridge's IP range --iptables=true Enable Docker's addition of iptables rules @@ -196,6 +196,16 @@ can be disabled with --ip-masq=false. +By default docker will assume all registries are securied via TLS. Prior versions +of docker used an auto fallback if a registry did not support TLS. This introduces +the opportunity for MITM attacks so in Docker 1.2 the user must specify `--insecure-registries` +when starting the Docker daemon to state which registries are not using TLS and to communicate +with these registries via plain text. If you are running a local registry over plain text +on `127.0.0.1:5000` you will be required to specify `--insecure-registries 127.0.0.1:500` +when starting the docker daemon to be able to push and pull images to that registry. +No automatic fallback will happen after Docker 1.2 to detect if a registry is using +HTTP or HTTPS. + Docker supports softlinks for the Docker data directory (`/var/lib/docker`) and for `/var/lib/docker/tmp`. The `DOCKER_TMPDIR` and the data directory can be set like this: From f43e77fc125840f40af52da01cd2a62bca49765e Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Wed, 20 Aug 2014 08:31:24 -0700 Subject: [PATCH 04/19] Don't hard code true for auth job Signed-off-by: Michael Crosby Conflicts: registry/service.go --- builtins/builtins.go | 4 ++-- docker/daemon.go | 7 +++++++ registry/service.go | 22 +++++++++++----------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/builtins/builtins.go b/builtins/builtins.go index f952d728b2..41bb249286 100644 --- a/builtins/builtins.go +++ b/builtins/builtins.go @@ -10,7 +10,6 @@ import ( "github.com/docker/docker/engine" "github.com/docker/docker/events" "github.com/docker/docker/pkg/parsers/kernel" - "github.com/docker/docker/registry" ) func Register(eng *engine.Engine) error { @@ -26,7 +25,8 @@ func Register(eng *engine.Engine) error { if err := eng.Register("version", dockerVersion); err != nil { return err } - return registry.NewService().Install(eng) + + return nil } // remote: a RESTful api for cross-docker communication diff --git a/docker/daemon.go b/docker/daemon.go index eef17efdc4..8b5826f344 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/engine" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/registry" ) const CanDaemon = true @@ -33,11 +34,17 @@ func mainDaemon() { } eng := engine.New() signal.Trap(eng.Shutdown) + // Load builtins if err := builtins.Register(eng); err != nil { log.Fatal(err) } + // load registry service + if err := registry.NewService(daemonCfg.InsecureRegistries).Install(eng); err != nil { + log.Fatal(err) + } + // load the daemon in the background so we can immediately start // the http api so that connections don't fail while the daemon // is booting diff --git a/registry/service.go b/registry/service.go index 334e7c2ed6..890837ca5e 100644 --- a/registry/service.go +++ b/registry/service.go @@ -13,12 +13,15 @@ import ( // 'pull': Download images from any registry (TODO) // 'push': Upload images to any registry (TODO) type Service struct { + insecureRegistries []string } // NewService returns a new instance of Service ready to be // installed no an engine. -func NewService() *Service { - return &Service{} +func NewService(insecureRegistries []string) *Service { + return &Service{ + insecureRegistries: insecureRegistries, + } } // Install installs registry capabilities to eng. @@ -32,15 +35,12 @@ func (s *Service) Install(eng *engine.Engine) error { // and returns OK if authentication was sucessful. // It can be used to verify the validity of a client's credentials. func (s *Service) Auth(job *engine.Job) engine.Status { - var ( - err error - authConfig = &AuthConfig{} - ) + var authConfig = new(AuthConfig) job.GetenvJson("authConfig", authConfig) - // TODO: this is only done here because auth and registry need to be merged into one pkg + if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() { - endpoint, err := NewEndpoint(addr, true) + endpoint, err := NewEndpoint(addr, IsSecure(addr, s.insecureRegistries)) if err != nil { return job.Error(err) } @@ -49,11 +49,11 @@ func (s *Service) Auth(job *engine.Job) engine.Status { } authConfig.ServerAddress = endpoint.String() } - status, err := Login(authConfig, HTTPRequestFactory(nil)) - if err != nil { + + if _, err := Login(authConfig, HTTPRequestFactory(nil)); err != nil { return job.Error(err) } - job.Printf("%s\n", status) + return engine.StatusOK } From e134f1f74a9d1f9ec55fcd8af8c2dce8e9558618 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Fri, 10 Oct 2014 23:22:12 -0400 Subject: [PATCH 05/19] Do not verify certificate when using --insecure-registry on an HTTPS registry Signed-off-by: Tibor Vass Conflicts: registry/registry.go registry/registry_test.go registry/service.go registry/session.go --- daemon/config.go | 2 +- docs/sources/reference/commandline/cli.md | 20 +-- graph/tags_unit_test.go | 2 +- registry/endpoint.go | 47 +++++-- registry/registry.go | 143 +++++++++------------- registry/registry_test.go | 4 +- registry/service.go | 5 +- registry/session.go | 2 +- 8 files changed, 113 insertions(+), 112 deletions(-) diff --git a/daemon/config.go b/daemon/config.go index bae0c8cd29..9e8d08e2a6 100644 --- a/daemon/config.go +++ b/daemon/config.go @@ -56,7 +56,7 @@ func (config *Config) InstallFlags() { flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b") flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking") flag.StringVar(&config.FixedCIDR, []string{"-fixed-cidr"}, "", "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)\nthis subnet must be nested in the bridge subnet (which is defined by -b or --bip)") - opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Make these registries use http") + opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback)") flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Enable inter-container communication") flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver") flag.StringVar(&config.ExecDriver, []string{"e", "-exec-driver"}, "native", "Force the Docker runtime to use a specific exec driver") diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index ddfc983d3a..4c863ee7bd 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -70,7 +70,7 @@ expect an integer, and they can only be specified once. -g, --graph="/var/lib/docker" Path to use as the root of the Docker runtime -H, --host=[] The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd. --icc=true Enable inter-container communication - --insecure-registry=[] Make these registries use http + --insecure-registry=[] Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) --ip=0.0.0.0 Default IP address to use when binding container ports --ip-forward=true Enable net.ipv4.ip_forward --ip-masq=true Enable IP masquerading for bridge's IP range @@ -195,16 +195,16 @@ to other machines on the Internet. This may interfere with some network topologi can be disabled with --ip-masq=false. +By default, Docker will assume all registries are secured via TLS with certificate verification +enabled. Prior versions of Docker used an auto fallback if a registry did not support TLS +(or if the TLS connection failed). This introduced the opportunity for Man In The Middle (MITM) +attacks, so as of Docker 1.3.1, the user must now specify the `--insecure-registry` daemon flag +for each insecure registry. An insecure registry is either not using TLS (i.e. plain text HTTP), +or is using TLS with a CA certificate not known by the Docker daemon (i.e. certification +verification disabled). For example, if there is a registry listening for HTTP at 127.0.0.1:5000, +as of Docker 1.3.1 you are required to specify `--insecure-registry 127.0.0.1:5000` when starting +the Docker daemon. -By default docker will assume all registries are securied via TLS. Prior versions -of docker used an auto fallback if a registry did not support TLS. This introduces -the opportunity for MITM attacks so in Docker 1.2 the user must specify `--insecure-registries` -when starting the Docker daemon to state which registries are not using TLS and to communicate -with these registries via plain text. If you are running a local registry over plain text -on `127.0.0.1:5000` you will be required to specify `--insecure-registries 127.0.0.1:500` -when starting the docker daemon to be able to push and pull images to that registry. -No automatic fallback will happen after Docker 1.2 to detect if a registry is using -HTTP or HTTPS. Docker supports softlinks for the Docker data directory (`/var/lib/docker`) and for `/var/lib/docker/tmp`. The `DOCKER_TMPDIR` and the data directory can be set like this: diff --git a/graph/tags_unit_test.go b/graph/tags_unit_test.go index e4f1fb809f..da512547d5 100644 --- a/graph/tags_unit_test.go +++ b/graph/tags_unit_test.go @@ -53,7 +53,7 @@ func mkTestTagStore(root string, t *testing.T) *TagStore { if err != nil { t.Fatal(err) } - store, err := NewTagStore(path.Join(root, "tags"), graph, nil) + store, err := NewTagStore(path.Join(root, "tags"), graph, nil, nil) if err != nil { t.Fatal(err) } diff --git a/registry/endpoint.go b/registry/endpoint.go index 5313a8079f..6dd4e1f60d 100644 --- a/registry/endpoint.go +++ b/registry/endpoint.go @@ -2,7 +2,6 @@ package registry import ( "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" @@ -34,9 +33,9 @@ func scanForApiVersion(hostname string) (string, APIVersion) { return hostname, DefaultAPIVersion } -func NewEndpoint(hostname string) (*Endpoint, error) { +func NewEndpoint(hostname string, secure bool) (*Endpoint, error) { var ( - endpoint Endpoint + endpoint = Endpoint{secure: secure} trimmedHostname string err error ) @@ -49,14 +48,27 @@ func NewEndpoint(hostname string) (*Endpoint, error) { return nil, err } + // Try HTTPS ping to registry endpoint.URL.Scheme = "https" if _, err := endpoint.Ping(); err != nil { - log.Debugf("Registry %s does not work (%s), falling back to http", endpoint, err) - // TODO: Check if http fallback is enabled - endpoint.URL.Scheme = "http" - if _, err = endpoint.Ping(); err != nil { - return nil, errors.New("Invalid Registry endpoint: " + err.Error()) + + //TODO: triggering highland build can be done there without "failing" + + if secure { + // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` + // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP. + return nil, fmt.Errorf("Invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) } + + // If registry is insecure and HTTPS failed, fallback to HTTP. + log.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err) + endpoint.URL.Scheme = "http" + _, err2 := endpoint.Ping() + if err2 == nil { + return &endpoint, nil + } + + return nil, fmt.Errorf("Invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) } return &endpoint, nil @@ -65,6 +77,7 @@ func NewEndpoint(hostname string) (*Endpoint, error) { type Endpoint struct { URL *url.URL Version APIVersion + secure bool } // Get the formated URL for the root of this registry Endpoint @@ -88,7 +101,7 @@ func (e Endpoint) Ping() (RegistryInfo, error) { return RegistryInfo{Standalone: false}, err } - resp, _, err := doRequest(req, nil, ConnectTimeout) + resp, _, err := doRequest(req, nil, ConnectTimeout, e.secure) if err != nil { return RegistryInfo{Standalone: false}, err } @@ -127,3 +140,19 @@ func (e Endpoint) Ping() (RegistryInfo, error) { log.Debugf("RegistryInfo.Standalone: %t", info.Standalone) return info, nil } + +// IsSecure returns false if the provided hostname is part of the list of insecure registries. +// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. +func IsSecure(hostname string, insecureRegistries []string) bool { + if hostname == IndexServerAddress() { + return true + } + + for _, h := range insecureRegistries { + if hostname == h { + return false + } + } + + return true +} diff --git a/registry/registry.go b/registry/registry.go index bcbce40198..15fed1b8a9 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/docker/docker/pkg/log" "github.com/docker/docker/utils" ) @@ -35,13 +36,17 @@ const ( ConnectTimeout ) -func newClient(jar http.CookieJar, roots *x509.CertPool, cert *tls.Certificate, timeout TimeoutType) *http.Client { +func newClient(jar http.CookieJar, roots *x509.CertPool, cert *tls.Certificate, timeout TimeoutType, secure bool) *http.Client { tlsConfig := tls.Config{RootCAs: roots} if cert != nil { tlsConfig.Certificates = append(tlsConfig.Certificates, *cert) } + if !secure { + tlsConfig.InsecureSkipVerify = true + } + httpTransport := &http.Transport{ DisableKeepAlives: true, Proxy: http.ProxyFromEnvironment, @@ -78,69 +83,76 @@ func newClient(jar http.CookieJar, roots *x509.CertPool, cert *tls.Certificate, } } -func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType) (*http.Response, *http.Client, error) { - hasFile := func(files []os.FileInfo, name string) bool { - for _, f := range files { - if f.Name() == name { - return true - } - } - return false - } - - hostDir := path.Join("/etc/docker/certs.d", req.URL.Host) - fs, err := ioutil.ReadDir(hostDir) - if err != nil && !os.IsNotExist(err) { - return nil, nil, err - } - +func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secure bool) (*http.Response, *http.Client, error) { var ( pool *x509.CertPool certs []*tls.Certificate ) - for _, f := range fs { - if strings.HasSuffix(f.Name(), ".crt") { - if pool == nil { - pool = x509.NewCertPool() + if secure && req.URL.Scheme == "https" { + hasFile := func(files []os.FileInfo, name string) bool { + for _, f := range files { + if f.Name() == name { + return true + } } - data, err := ioutil.ReadFile(path.Join(hostDir, f.Name())) - if err != nil { - return nil, nil, err - } - pool.AppendCertsFromPEM(data) + return false } - if strings.HasSuffix(f.Name(), ".cert") { - certName := f.Name() - keyName := certName[:len(certName)-5] + ".key" - if !hasFile(fs, keyName) { - return nil, nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName) - } - cert, err := tls.LoadX509KeyPair(path.Join(hostDir, certName), path.Join(hostDir, keyName)) - if err != nil { - return nil, nil, err - } - certs = append(certs, &cert) + + hostDir := path.Join("/etc/docker/certs.d", req.URL.Host) + log.Debugf("hostDir: %s", hostDir) + fs, err := ioutil.ReadDir(hostDir) + if err != nil && !os.IsNotExist(err) { + return nil, nil, err } - if strings.HasSuffix(f.Name(), ".key") { - keyName := f.Name() - certName := keyName[:len(keyName)-4] + ".cert" - if !hasFile(fs, certName) { - return nil, nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName) + + for _, f := range fs { + if strings.HasSuffix(f.Name(), ".crt") { + if pool == nil { + pool = x509.NewCertPool() + } + log.Debugf("crt: %s", hostDir+"/"+f.Name()) + data, err := ioutil.ReadFile(path.Join(hostDir, f.Name())) + if err != nil { + return nil, nil, err + } + pool.AppendCertsFromPEM(data) + } + if strings.HasSuffix(f.Name(), ".cert") { + certName := f.Name() + keyName := certName[:len(certName)-5] + ".key" + log.Debugf("cert: %s", hostDir+"/"+f.Name()) + if !hasFile(fs, keyName) { + return nil, nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName) + } + cert, err := tls.LoadX509KeyPair(path.Join(hostDir, certName), path.Join(hostDir, keyName)) + if err != nil { + return nil, nil, err + } + certs = append(certs, &cert) + } + if strings.HasSuffix(f.Name(), ".key") { + keyName := f.Name() + certName := keyName[:len(keyName)-4] + ".cert" + log.Debugf("key: %s", hostDir+"/"+f.Name()) + if !hasFile(fs, certName) { + return nil, nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName) + } } } } if len(certs) == 0 { - client := newClient(jar, pool, nil, timeout) + client := newClient(jar, pool, nil, timeout, secure) res, err := client.Do(req) if err != nil { return nil, nil, err } return res, client, nil } + for i, cert := range certs { - client := newClient(jar, pool, cert, timeout) + client := newClient(jar, pool, cert, timeout, secure) res, err := client.Do(req) // If this is the last cert, otherwise, continue to next cert if 403 or 5xx if i == len(certs)-1 || err == nil && res.StatusCode != 403 && res.StatusCode < 500 { @@ -202,49 +214,6 @@ func ResolveRepositoryName(reposName string) (string, string, error) { return hostname, reposName, nil } -// this method expands the registry name as used in the prefix of a repo -// to a full url. if it already is a url, there will be no change. -func ExpandAndVerifyRegistryUrl(hostname string, secure bool) (string, error) { - if hostname == IndexServerAddress() { - return hostname, nil - } - - endpoint := fmt.Sprintf("http://%s/v1/", hostname) - - if secure { - endpoint = fmt.Sprintf("https://%s/v1/", hostname) - } - - if _, oerr := pingRegistryEndpoint(endpoint); oerr != nil { - //TODO: triggering highland build can be done there without "failing" - err := fmt.Errorf("Invalid registry endpoint '%s': %s ", endpoint, oerr) - - if secure { - err = fmt.Errorf("%s. If this private registry supports only HTTP, please add `--insecure-registry %s` to the daemon's arguments.", oerr, hostname) - } - - return "", err - } - - return endpoint, nil -} - -// this method verifies if the provided hostname is part of the list of -// insecure registries and returns false if HTTP should be used -func IsSecure(hostname string, insecureRegistries []string) bool { - if hostname == IndexServerAddress() { - return true - } - - for _, h := range insecureRegistries { - if hostname == h { - return false - } - } - - return true -} - func trustedLocation(req *http.Request) bool { var ( trusteds = []string{"docker.com", "docker.io"} diff --git a/registry/registry_test.go b/registry/registry_test.go index ab4178126a..c9a9fc81b7 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -18,7 +18,7 @@ var ( func spawnTestRegistrySession(t *testing.T) *Session { authConfig := &AuthConfig{} - endpoint, err := NewEndpoint(makeURL("/v1/")) + endpoint, err := NewEndpoint(makeURL("/v1/"), false) if err != nil { t.Fatal(err) } @@ -30,7 +30,7 @@ func spawnTestRegistrySession(t *testing.T) *Session { } func TestPingRegistryEndpoint(t *testing.T) { - ep, err := NewEndpoint(makeURL("/v1/")) + ep, err := NewEndpoint(makeURL("/v1/"), false) if err != nil { t.Fatal(err) } diff --git a/registry/service.go b/registry/service.go index 890837ca5e..32274f407d 100644 --- a/registry/service.go +++ b/registry/service.go @@ -89,7 +89,10 @@ func (s *Service) Search(job *engine.Job) engine.Status { if err != nil { return job.Error(err) } - endpoint, err := NewEndpoint(hostname) + + secure := IsSecure(hostname, s.insecureRegistries) + + endpoint, err := NewEndpoint(hostname, secure) if err != nil { return job.Error(err) } diff --git a/registry/session.go b/registry/session.go index 5067b8d5de..28959967de 100644 --- a/registry/session.go +++ b/registry/session.go @@ -64,7 +64,7 @@ func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, endpo } func (r *Session) doRequest(req *http.Request) (*http.Response, *http.Client, error) { - return doRequest(req, r.jar, r.timeout) + return doRequest(req, r.jar, r.timeout, r.indexEndpoint.secure) } // Retrieve the history of a given image from the Registry. From ada9ac7b1300d2c647a17f5f5f274195ef5620b6 Mon Sep 17 00:00:00 2001 From: Jessica Frazelle Date: Thu, 16 Oct 2014 11:39:22 -0700 Subject: [PATCH 06/19] Setting iptables=false should propagate to ip-masq=false Signed-off-by: Jessica Frazelle --- daemon/daemon.go | 2 +- integration-cli/docker_cli_daemon_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index d3c79c871c..d069797ac4 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -731,7 +731,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error) return nil, fmt.Errorf("You specified --iptables=false with --icc=false. ICC uses iptables to function. Please set --icc or --iptables to true.") } if !config.EnableIptables && config.EnableIpMasq { - return nil, fmt.Errorf("You specified --iptables=false with --ipmasq=true. IP masquerading uses iptables to function. Please set --ipmasq to false or --iptables to true.") + config.EnableIpMasq = false } config.DisableNetwork = config.BridgeIface == disableNetworkBridge diff --git a/integration-cli/docker_cli_daemon_test.go b/integration-cli/docker_cli_daemon_test.go index 906680dc6d..6160e57e94 100644 --- a/integration-cli/docker_cli_daemon_test.go +++ b/integration-cli/docker_cli_daemon_test.go @@ -82,3 +82,13 @@ func TestDaemonRestartWithVolumesRefs(t *testing.T) { logDone("daemon - volume refs are restored") } + +func TestDaemonStartIptablesFalse(t *testing.T) { + d := NewDaemon(t) + if err := d.Start("--iptables=false"); err != nil { + t.Fatalf("we should have been able to start the daemon with passing iptables=false: %v", err) + } + d.Stop() + + logDone("daemon - started daemon with iptables=false") +} From 7d9ccc2636af30e45950b27d3f69cacb0ad43f40 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Thu, 16 Oct 2014 10:14:26 -0600 Subject: [PATCH 07/19] Fix more missing HOME references Signed-off-by: Andrew Page --- hack/release.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hack/release.sh b/hack/release.sh index da832e8bb8..1174e0cc4c 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -270,7 +270,7 @@ EOF done # Upload keys - s3cmd sync /.gnupg/ s3://$BUCKET/ubuntu/.gnupg/ + s3cmd sync $HOME/.gnupg/ s3://$BUCKET/ubuntu/.gnupg/ gpg --armor --export releasedocker > bundles/$VERSION/ubuntu/gpg s3cmd --acl-public put bundles/$VERSION/ubuntu/gpg s3://$BUCKET/gpg @@ -355,8 +355,8 @@ release_test() { setup_gpg() { # Make sure that we have our keys - mkdir -p /.gnupg/ - s3cmd sync s3://$BUCKET/ubuntu/.gnupg/ /.gnupg/ || true + mkdir -p $HOME/.gnupg/ + s3cmd sync s3://$BUCKET/ubuntu/.gnupg/ $HOME/.gnupg/ || true gpg --list-keys releasedocker >/dev/null || { gpg --gen-key --batch < Date: Wed, 15 Oct 2014 22:39:51 -0400 Subject: [PATCH 08/19] Avoid fallback to SSL protocols < TLS1.0 Signed-off-by: Tibor Vass Docker-DCO-1.1-Signed-off-by: Daniel, Dao Quang Minh (github: dqminh) Conflicts: registry/registry.go --- api/server/server.go | 2 ++ docker/docker.go | 2 ++ registry/registry.go | 6 +++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/server/server.go b/api/server/server.go index 897dd6142f..93b8b60a8f 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -1439,6 +1439,8 @@ func ListenAndServe(proto, addr string, job *engine.Job) error { tlsConfig := &tls.Config{ NextProtos: []string{"http/1.1"}, Certificates: []tls.Certificate{cert}, + // Avoid fallback on insecure SSL protocols + MinVersion: tls.VersionTLS10, } if job.GetenvBool("TlsVerify") { certPool := x509.NewCertPool() diff --git a/docker/docker.go b/docker/docker.go index 37cd155bb7..f0cbb6f6ab 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -93,6 +93,8 @@ func main() { } tlsConfig.Certificates = []tls.Certificate{cert} } + // Avoid fallback to SSL protocols < TLS1.0 + tlsConfig.MinVersion = tls.VersionTLS10 } if *flTls || *flTlsVerify { diff --git a/registry/registry.go b/registry/registry.go index 15fed1b8a9..a03790af03 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -37,7 +37,11 @@ const ( ) func newClient(jar http.CookieJar, roots *x509.CertPool, cert *tls.Certificate, timeout TimeoutType, secure bool) *http.Client { - tlsConfig := tls.Config{RootCAs: roots} + tlsConfig := tls.Config{ + RootCAs: roots, + // Avoid fallback to SSL protocols < TLS1.0 + MinVersion: tls.VersionTLS10, + } if cert != nil { tlsConfig.Certificates = append(tlsConfig.Certificates, *cert) From cf23053eb1d88ce7b3ba2ca06f42c408021c93b5 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Fri, 17 Oct 2014 19:15:07 +0000 Subject: [PATCH 09/19] builder: fix escaping for ENV variables. Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) --- builder/support.go | 14 +++++- integration-cli/docker_cli_build_test.go | 55 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/builder/support.go b/builder/support.go index a084190f2c..6c7ac4096e 100644 --- a/builder/support.go +++ b/builder/support.go @@ -10,13 +10,25 @@ var ( // `\$` - match literal $ // `[[:alnum:]_]+` - match things like `$SOME_VAR` // `{[[:alnum:]_]+}` - match things like `${SOME_VAR}` - tokenEnvInterpolation = regexp.MustCompile(`(\\\\+|[^\\]|\b|\A)\$([[:alnum:]_]+|{[[:alnum:]_]+})`) + tokenEnvInterpolation = regexp.MustCompile(`(\\|\\\\+|[^\\]|\b|\A)\$([[:alnum:]_]+|{[[:alnum:]_]+})`) // this intentionally punts on more exotic interpolations like ${SOME_VAR%suffix} and lets the shell handle those directly ) // handle environment replacement. Used in dispatcher. func (b *Builder) replaceEnv(str string) string { for _, match := range tokenEnvInterpolation.FindAllString(str, -1) { + idx := strings.Index(match, "\\$") + if idx != -1 { + if idx+2 >= len(match) { + str = strings.Replace(str, match, "\\$", -1) + continue + } + + stripped := match[idx+2:] + str = strings.Replace(str, match, "$"+stripped, -1) + continue + } + match = match[strings.Index(match, "$"):] matchKey := strings.Trim(match, "${}") diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index e456d579ab..ac792df008 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -15,6 +15,61 @@ import ( "github.com/docker/docker/pkg/archive" ) +func TestBuildEnvEscapes(t *testing.T) { + name := "testbuildenvescapes" + defer deleteAllContainers() + defer deleteImages(name) + _, err := buildImage(name, + ` + FROM busybox + ENV TEST foo + CMD echo \$ + `, + true) + + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-t", name)) + + if err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(out) != "$" { + t.Fatalf("Env TEST was not overwritten with bar when foo was supplied to dockerfile: was %q", strings.TrimSpace(out)) + } + + logDone("build - env should handle \\$ properly") +} + +func TestBuildEnvOverwrite(t *testing.T) { + name := "testbuildenvoverwrite" + defer deleteAllContainers() + defer deleteImages(name) + + _, err := buildImage(name, + ` + FROM busybox + ENV TEST foo + CMD echo \${TEST} + `, + true) + + if err != nil { + t.Fatal(err) + } + + out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "run", "-e", "TEST=bar", "-t", name)) + + if err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(out) != "bar" { + t.Fatalf("Env TEST was not overwritten with bar when foo was supplied to dockerfile: was %q", strings.TrimSpace(out)) + } + + logDone("build - env should overwrite builder ENV during run") +} + func TestBuildOnBuildForbiddenMaintainerInSourceImage(t *testing.T) { name := "testbuildonbuildforbiddenmaintainerinsourceimage" defer deleteImages(name) From ff325bcb2feb501c2287ecb305644b003784f726 Mon Sep 17 00:00:00 2001 From: Alexandr Morozov Date: Fri, 17 Oct 2014 11:06:05 -0700 Subject: [PATCH 10/19] Don't write pull output to stdout on container creating Fixes #8632 Signed-off-by: Alexandr Morozov --- api/client/commands.go | 9 +++++++-- integration-cli/docker_cli_run_test.go | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/api/client/commands.go b/api/client/commands.go index 6c4e5c55fe..2c44bb63c5 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -1986,6 +1986,10 @@ func (cli *DockerCli) CmdTag(args ...string) error { } func (cli *DockerCli) pullImage(image string) error { + return cli.pullImageCustomOut(image, cli.out) +} + +func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error { v := url.Values{} repos, tag := parsers.ParseRepositoryTag(image) // pull only the image tagged 'latest' if no tag was specified @@ -2014,7 +2018,7 @@ func (cli *DockerCli) pullImage(image string) error { registryAuthHeader := []string{ base64.URLEncoding.EncodeToString(buf), } - if err = cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out, map[string][]string{"X-Registry-Auth": registryAuthHeader}); err != nil { + if err = cli.stream("POST", "/images/create?"+v.Encode(), nil, out, map[string][]string{"X-Registry-Auth": registryAuthHeader}); err != nil { return err } return nil @@ -2081,7 +2085,8 @@ func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runc if statusCode == 404 { fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", config.Image) - if err = cli.pullImage(config.Image); err != nil { + // we don't want to write to stdout anything apart from container.ID + if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil { return nil, err } // Retry diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index d50f6f3443..83f03fd9f8 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "bytes" "fmt" "io/ioutil" "net" @@ -2374,3 +2375,18 @@ func TestRunVolumesNotRecreatedOnStart(t *testing.T) { logDone("run - volumes not recreated on start") } + +func TestRunNoOutputFromPullInStdout(t *testing.T) { + defer deleteAllContainers() + // just run with unknown image + cmd := exec.Command(dockerBinary, "run", "asdfsg") + stdout := bytes.NewBuffer(nil) + cmd.Stdout = stdout + if err := cmd.Run(); err == nil { + t.Fatal("Run with unknown image should fail") + } + if stdout.Len() != 0 { + t.Fatalf("Stdout contains output from pull: %s", stdout) + } + logDone("run - no output from pull in stdout") +} From 66fba7c46ed3507166774ec8000ca478907e80a7 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 20 Oct 2014 15:27:26 -0400 Subject: [PATCH 11/19] Clean volume paths Fixes #8659 Signed-off-by: Brian Goff --- daemon/volumes.go | 3 ++ integration-cli/docker_cli_run_test.go | 50 ++++++++++++++++++++++++++ integration-cli/docker_test_vars.go | 6 ++-- integration-cli/docker_utils.go | 10 ++++++ volumes/repository.go | 5 +-- 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/daemon/volumes.go b/daemon/volumes.go index c7a8d7bfcb..b34d9678cb 100644 --- a/daemon/volumes.go +++ b/daemon/volumes.go @@ -133,6 +133,7 @@ func (container *Container) parseVolumeMountConfig() (map[string]*Mount, error) // Get the rest of the volumes for path := range container.Config.Volumes { // Check if this is already added as a bind-mount + path = filepath.Clean(path) if _, exists := mounts[path]; exists { continue } @@ -182,6 +183,8 @@ func parseBindMountSpec(spec string) (string, string, bool, error) { return "", "", false, fmt.Errorf("cannot bind mount volume: %s volume paths must be absolute.", path) } + path = filepath.Clean(path) + mountToPath = filepath.Clean(mountToPath) return path, mountToPath, writable, nil } diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 83f03fd9f8..417368f4f0 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2390,3 +2390,53 @@ func TestRunNoOutputFromPullInStdout(t *testing.T) { } logDone("run - no output from pull in stdout") } + +func TestRunVolumesCleanPaths(t *testing.T) { + defer deleteAllContainers() + + if _, err := buildImage("run_volumes_clean_paths", + `FROM busybox + VOLUME /foo/`, + true); err != nil { + t.Fatal(err) + } + defer deleteImages("run_volumes_clean_paths") + + cmd := exec.Command(dockerBinary, "run", "-v", "/foo", "-v", "/bar/", "--name", "dark_helmet", "run_volumes_clean_paths") + if out, _, err := runCommandWithOutput(cmd); err != nil { + t.Fatal(err, out) + } + + out, err := inspectFieldMap("dark_helmet", "Volumes", "/foo/") + if err != nil { + t.Fatal(err) + } + if out != "" { + t.Fatalf("Found unexpected volume entry for '/foo/' in volumes\n%q", out) + } + + out, err = inspectFieldMap("dark_helmet", "Volumes", "/foo") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, volumesStoragePath) { + t.Fatalf("Volume was not defined for /foo\n%q", out) + } + + out, err = inspectFieldMap("dark_helmet", "Volumes", "/bar/") + if err != nil { + t.Fatal(err) + } + if out != "" { + t.Fatalf("Found unexpected volume entry for '/bar/' in volumes\n%q", out) + } + out, err = inspectFieldMap("dark_helmet", "Volumes", "/bar") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, volumesStoragePath) { + t.Fatalf("Volume was not defined for /bar\n%q", out) + } + + logDone("run - volume paths are cleaned") +} diff --git a/integration-cli/docker_test_vars.go b/integration-cli/docker_test_vars.go index fdbcf073ec..23903a39a9 100644 --- a/integration-cli/docker_test_vars.go +++ b/integration-cli/docker_test_vars.go @@ -16,8 +16,10 @@ var ( // the private registry to use for tests privateRegistryURL = "127.0.0.1:5000" - execDriverPath = "/var/lib/docker/execdriver/native" - volumesConfigPath = "/var/lib/docker/volumes" + dockerBasePath = "/var/lib/docker" + execDriverPath = dockerBasePath + "/execdriver/native" + volumesConfigPath = dockerBasePath + "/volumes" + volumesStoragePath = dockerBasePath + "/vfs/dir" workingDirectory string ) diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index 17370eb707..3bdf36ec19 100644 --- a/integration-cli/docker_utils.go +++ b/integration-cli/docker_utils.go @@ -507,6 +507,16 @@ func inspectFieldJSON(name, field string) (string, error) { return strings.TrimSpace(out), nil } +func inspectFieldMap(name, path, field string) (string, error) { + format := fmt.Sprintf("{{index .%s %q}}", path, field) + inspectCmd := exec.Command(dockerBinary, "inspect", "-f", format, name) + out, exitCode, err := runCommandWithOutput(inspectCmd) + if err != nil || exitCode != 0 { + return "", fmt.Errorf("failed to inspect %s: %s", name, out) + } + return strings.TrimSpace(out), nil +} + func getIDByName(name string) (string, error) { return inspectField(name, "Id") } diff --git a/volumes/repository.go b/volumes/repository.go index e765d944c2..2383f34a93 100644 --- a/volumes/repository.go +++ b/volumes/repository.go @@ -55,6 +55,7 @@ func (r *Repository) newVolume(path string, writable bool) (*Volume, error) { return nil, err } } + path = filepath.Clean(path) path, err = filepath.EvalSymlinks(path) if err != nil { @@ -126,7 +127,7 @@ func (r *Repository) get(path string) *Volume { if err != nil { return nil } - return r.volumes[path] + return r.volumes[filepath.Clean(path)] } func (r *Repository) Add(volume *Volume) error { @@ -160,7 +161,7 @@ func (r *Repository) Delete(path string) error { if err != nil { return err } - volume := r.get(path) + volume := r.get(filepath.Clean(path)) if volume == nil { return fmt.Errorf("Volume %s does not exist", path) } From 21ab75afe0f7e92ffe942e30c77651a01b6db223 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Tue, 21 Oct 2014 19:26:20 +0000 Subject: [PATCH 12/19] builder: handle cases where onbuild is not uppercase. Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) --- builder/dispatchers.go | 3 +- integration-cli/docker_cli_build_test.go | 35 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/builder/dispatchers.go b/builder/dispatchers.go index 82bb6ce5fd..0c2a580872 100644 --- a/builder/dispatchers.go +++ b/builder/dispatchers.go @@ -11,6 +11,7 @@ import ( "fmt" "io/ioutil" "path/filepath" + "regexp" "strings" "github.com/docker/docker/nat" @@ -129,7 +130,7 @@ func onbuild(b *Builder, args []string, attributes map[string]bool, original str return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction) } - original = strings.TrimSpace(strings.TrimLeft(original, "ONBUILD")) + original = regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(original, "") b.Config.OnBuild = append(b.Config.OnBuild, original) return b.commit("", b.Config.Cmd, fmt.Sprintf("ONBUILD %s", original)) diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index ac792df008..808f64c7e9 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -15,6 +15,41 @@ import ( "github.com/docker/docker/pkg/archive" ) +func TestBuildOnBuildLowercase(t *testing.T) { + name := "testbuildonbuildlowercase" + name2 := "testbuildonbuildlowercase2" + + defer deleteImages(name, name2) + + _, err := buildImage(name, + ` + FROM busybox + onbuild run echo quux + `, true) + + if err != nil { + t.Fatal(err) + } + + _, out, err := buildImageWithOut(name2, fmt.Sprintf(` + FROM %s + `, name), true) + + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(out, "quux") { + t.Fatalf("Did not receive the expected echo text, got %s", out) + } + + if strings.Contains(out, "ONBUILD ONBUILD") { + t.Fatalf("Got an ONBUILD ONBUILD error with no error: got %s", out) + } + + logDone("build - handle case-insensitive onbuild statement") +} + func TestBuildEnvEscapes(t *testing.T) { name := "testbuildenvescapes" defer deleteAllContainers() From 3d287811d78c373a4fd3ef069a9c13cd83b80169 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Wed, 22 Oct 2014 08:30:48 -0400 Subject: [PATCH 13/19] Docs edits for dropping SSLv3 and under + release notes for 1.3.1 Signed-off-by: Tibor Vass Conflicts: docs/sources/index.md --- docs/mkdocs.yml | 1 + docs/sources/index.md | 38 +-- .../reference/api/hub_registry_spec.md | 30 +- docs/sources/reference/api/registry_api.md | 29 +- docs/sources/reference/commandline/cli.md | 7 +- docs/sources/release-notes.md | 291 ++++++++++++++++++ 6 files changed, 334 insertions(+), 62 deletions(-) mode change 100755 => 100644 docs/mkdocs.yml create mode 100644 docs/sources/release-notes.md diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml old mode 100755 new mode 100644 index 5ea8d56e60..25f84b5a08 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -26,6 +26,7 @@ pages: # Introduction: - ['index.md', 'About', 'Docker'] +- ['release-notes.md', 'About', 'Release Notes'] - ['introduction/index.md', '**HIDDEN**'] - ['introduction/understanding-docker.md', 'About', 'Understanding Docker'] diff --git a/docs/sources/index.md b/docs/sources/index.md index 7e60b0dc6d..949fe72e0a 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -88,40 +88,4 @@ implementation, check out the [Docker User Guide](/userguide/). ## Release Notes -**Version 1.3.0** - -This version fixes a number of bugs and issues and adds new functions and other -improvements. These include: - -*New command: `docker exec`* - -The new `docker exec` command lets you run a process in an existing, active -container. The command has APIs for both the daemon and the client. With -`docker exec`, you'll be able to do things like add or remove devices from running containers, debug running containers, and run commands that are not -part of the container's static specification. - -*New command: `docker create`* - -Traditionally, the `docker run` command has been used to both create a -container and spawn a process to run it. The new `docker create` command breaks -this apart, letting you set up a container without actually starting it. This -provides more control over management of the container lifecycle, giving you the -ability to configure things like volumes or port mappings before the container -is started. For example, in a rapid-response scaling situation, you could use -`create` to prepare and stage ten containers in anticipation of heavy loads. - -*New provenance features* - -Official images are now signed by Docker, Inc. to improve your confidence and -security. Look for the blue ribbons on the [Docker Hub](https://hub.docker.com/). -The Docker Engine has been updated to automatically verify that a given Official -Repo has a current, valid signature. If no valid signature is detected, Docker -Engine will use a prior image. - - -*Other improvements & changes* - -We've added a new security options flag that lets you set SELinux and AppArmor -labels and profiles. This means you'll longer have to use `docker run ---privileged on kernels that support SE Linux or AppArmor. - +A summary of the changes in each release in the current series can now be found on the separate [Release Notes page](/release-notes/) diff --git a/docs/sources/reference/api/hub_registry_spec.md b/docs/sources/reference/api/hub_registry_spec.md index ee15277a44..853eda4aee 100644 --- a/docs/sources/reference/api/hub_registry_spec.md +++ b/docs/sources/reference/api/hub_registry_spec.md @@ -4,7 +4,9 @@ page_keywords: docker, registry, api, hub # The Docker Hub and the Registry spec -## The 3 roles +## The three roles + +There are three major components playing a role in the Docker ecosystem. ### Docker Hub @@ -21,13 +23,15 @@ The Docker Hub has different components: - Authentication service - Tokenization -The Docker Hub is authoritative for those information. +The Docker Hub is authoritative for that information. -We expect that there will be only one instance of the Docker Hub, run and +There is only one instance of the Docker Hub, run and managed by Docker Inc. ### Registry +The registry has the following characteristics: + - It stores the images and the graph for a set of repositories - It does not have user accounts data - It has no notion of user accounts or authorization @@ -37,35 +41,35 @@ managed by Docker Inc. - It doesn't have a local database - [Source Code](https://github.com/docker/docker-registry) -We expect that there will be multiple registries out there. To help to +We expect that there will be multiple registries out there. To help you grasp the context, here are some examples of registries: - **sponsor registry**: such a registry is provided by a third-party hosting infrastructure as a convenience for their customers and the - docker community as a whole. Its costs are supported by the third + Docker community as a whole. Its costs are supported by the third party, but the management and operation of the registry are - supported by dotCloud. It features read/write access, and delegates + supported by Docker, Inc. It features read/write access, and delegates authentication and authorization to the Docker Hub. - **mirror registry**: such a registry is provided by a third-party hosting infrastructure but is targeted at their customers only. Some mechanism (unspecified to date) ensures that public images are pulled from a sponsor registry to the mirror registry, to make sure - that the customers of the third-party provider can “docker pull” + that the customers of the third-party provider can `docker pull` those images locally. - **vendor registry**: such a registry is provided by a software - vendor, who wants to distribute docker images. It would be operated + vendor who wants to distribute docker images. It would be operated and managed by the vendor. Only users authorized by the vendor would be able to get write access. Some images would be public (accessible for anyone), others private (accessible only for authorized users). Authentication and authorization would be delegated to the Docker Hub. - The goal of vendor registries is to let someone do “docker pull - basho/riak1.3” and automatically push from the vendor registry - (instead of a sponsor registry); i.e. get all the convenience of a + The goal of vendor registries is to let someone do `docker pull + basho/riak1.3` and automatically push from the vendor registry + (instead of a sponsor registry); i.e., vendors get all the convenience of a sponsor registry, while retaining control on the asset distribution. - **private registry**: such a registry is located behind a firewall, or protected by an additional security layer (HTTP authorization, SSL client-side certificates, IP address authorization...). The - registry is operated by a private entity, outside of dotCloud's + registry is operated by a private entity, outside of Docker's control. It can optionally delegate additional authorization to the Docker Hub, but it is not mandatory. @@ -77,7 +81,7 @@ grasp the context, here are some examples of registries: > - local mount point; > - remote docker addressed through SSH. -The latter would only require two new commands in docker, e.g., +The latter would only require two new commands in Docker, e.g., `registryget` and `registryput`, wrapping access to the local filesystem (and optionally doing consistency checks). Authentication and authorization are then delegated diff --git a/docs/sources/reference/api/registry_api.md b/docs/sources/reference/api/registry_api.md index 1ae37dba6d..d6130bf767 100644 --- a/docs/sources/reference/api/registry_api.md +++ b/docs/sources/reference/api/registry_api.md @@ -21,30 +21,30 @@ grasp the context, here are some examples of registries: - **sponsor registry**: such a registry is provided by a third-party hosting infrastructure as a convenience for their customers and the - docker community as a whole. Its costs are supported by the third + Docker community as a whole. Its costs are supported by the third party, but the management and operation of the registry are - supported by dotCloud. It features read/write access, and delegates + supported by Docker. It features read/write access, and delegates authentication and authorization to the Index. - **mirror registry**: such a registry is provided by a third-party hosting infrastructure but is targeted at their customers only. Some mechanism (unspecified to date) ensures that public images are pulled from a sponsor registry to the mirror registry, to make sure - that the customers of the third-party provider can “docker pull” + that the customers of the third-party provider can `docker pull` those images locally. - **vendor registry**: such a registry is provided by a software - vendor, who wants to distribute docker images. It would be operated + vendor, who wants to distribute Docker images. It would be operated and managed by the vendor. Only users authorized by the vendor would be able to get write access. Some images would be public (accessible for anyone), others private (accessible only for authorized users). Authentication and authorization would be delegated to the Index. - The goal of vendor registries is to let someone do “docker pull - basho/riak1.3” and automatically push from the vendor registry - (instead of a sponsor registry); i.e. get all the convenience of a + The goal of vendor registries is to let someone do `docker pull + basho/riak1.3` and automatically push from the vendor registry + (instead of a sponsor registry); i.e., get all the convenience of a sponsor registry, while retaining control on the asset distribution. - **private registry**: such a registry is located behind a firewall, or protected by an additional security layer (HTTP authorization, SSL client-side certificates, IP address authorization...). The - registry is operated by a private entity, outside of dotCloud's + registry is operated by a private entity, outside of Docker's control. It can optionally delegate additional authorization to the Index, but it is not mandatory. @@ -52,7 +52,7 @@ grasp the context, here are some examples of registries: > Mirror registries and private registries which do not use the Index > don't even need to run the registry code. They can be implemented by any > kind of transport implementing HTTP GET and PUT. Read-only registries -> can be powered by a simple static HTTP server. +> can be powered by a simple static HTTPS server. > **Note**: > The latter implies that while HTTP is the protocol of choice for a registry, @@ -60,13 +60,20 @@ grasp the context, here are some examples of registries: > > - HTTP with GET (and PUT for read-write registries); > - local mount point; -> - remote docker addressed through SSH. +> - remote Docker addressed through SSH. -The latter would only require two new commands in docker, e.g., +The latter would only require two new commands in Docker, e.g., `registryget` and `registryput`, wrapping access to the local filesystem (and optionally doing consistency checks). Authentication and authorization are then delegated to SSH (e.g., with public keys). +> **Note**: +> Private registry servers that expose an HTTP endpoint need to be secured with +> TLS (preferably TLSv1.2, but at least TLSv1.0). Make sure to put the CA +> certificate at /etc/docker/certs.d/my.registry.com:5000/ca.crt on the Docker +> host, so that the daemon can securely access the private registry. +> Support for SSLv3 and lower is not available due to security issues. + The default namespace for a private repository is `library`. # Endpoints diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 4c863ee7bd..a57b4cc28e 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -112,7 +112,12 @@ direct access to the Docker daemon - and should be secured either using the [built in https encrypted socket](/articles/https/), or by putting a secure web proxy in front of it. You can listen on port `2375` on all network interfaces with `-H tcp://0.0.0.0:2375`, or on a particular network interface using its IP -address: `-H tcp://192.168.59.103:2375`. +address: `-H tcp://192.168.59.103:2375`. It is conventional to use port `2375` +for un-encrypted, and port `2376` for encrypted communication with the daemon. + +> **Note** If you're using an HTTPS encrypted socket, keep in mind that only TLS1.0 +> and greater are supported. Protocols SSLv3 and under are not supported anymore +> for security reasons. On Systemd based systems, you can communicate with the daemon via [systemd socket activation](http://0pointer.de/blog/projects/socket-activation.html), use diff --git a/docs/sources/release-notes.md b/docs/sources/release-notes.md new file mode 100644 index 0000000000..6b2f0fd571 --- /dev/null +++ b/docs/sources/release-notes.md @@ -0,0 +1,291 @@ +page_title: Docker 1.x Series Release Notes page_description: Release Notes for +Docker 1.x. page_keywords: docker, documentation, about, technology, +understanding, release + +#Release Notes + +##Version 1.3.1 +(2014-10-28) + +This release fixes some bugs and addresses some security issues. + +*Security fixes* + +Patches and changes were made to address CVE-2014-5277 and CVE-2014-3566. Specifically, changes were made to: +* Prevent fallback to SSL protocols < TLS 1.0 for client, daemon and registry +* Secure HTTPS connection to registries with certificate verification and without HTTP fallback unless `--insecure-registry` is specified. + +*Runtime fixes* + +* Fixed issue where volumes would not be shared + +*Client fixes* + +* Fixed issue with `--iptables=false` not automatically setting +`--ip-masq=false` +* Fixed docker run output to non-TTY stdout + +*Builder fixes* + +* Fixed escaping `$` for environment variables +* Fixed issue with lowercase `onbuild` Dockerfile instruction + + +##Version 1.3.0 + +This version fixes a number of bugs and issues and adds new functions and other +improvements. The [GitHub 1.3milestone](https://github.com/docker/docker/issues?q=milestone%3A1.3.0+) has +more detailed information. Major additions and changes include: + +###New Features + +*New command: `docker exec`* + +The new `docker exec` command lets you run a process in an existing, active +container. The command has APIs for both the daemon and the client. With `docker +exec`, you'll be able to do things like add or remove devices from running +containers, debug running containers, and run commands that are not part of the +container's static specification. Details in the [command line reference](/reference/commandline/cli/#exec). + +*New command: `docker create`* + +Traditionally, the `docker run` command has been used to both create a container +and spawn a process to run it. The new `docker create` command breaks this +apart, letting you set up a container without actually starting it. This +provides more control over management of the container lifecycle, giving you the +ability to configure things like volumes or port mappings before the container +is started. For example, in a rapid-response scaling situation, you could use +`create` to prepare and stage ten containers in anticipation of heavy loads. +Details in the [command line reference](/reference/commandline/cli/#create). + +*Tech preview of new provenance features* + +This release offers a sneak peek at new image signing capabilities that are +currently under development. Soon, these capabilities will allow any image +author to sign their images to certify they have not been tampered with. For +this release, Official images are now signed by Docker, Inc. Not only does this +demonstrate the new functionality, we hope it will improve your confidence in +the security of Official images. Look for the blue ribbons denoting signed +images on the [Docker Hub](https://hub.docker.com/). The Docker Engine has been +updated to automatically verify that a given Official Repo has a current, valid +signature. When pulling a signed image, you'll see a message stating `the image +you are pulling has been verified`. If no valid signature is detected, Docker +Engine will fall back to pulling a regular, unsigned image. + +###Other improvements & changes* + +* We've added a new security options flag to the `docker run` command, +`--security-opt`, that lets you set SELinux and AppArmor labels and profiles. +This means you'll no longer have to use `docker run --privileged` on kernels +that support SE Linux or AppArmor. For more information, see the [command line +reference](/reference/commandline/cli/#run). + +* A new flag, `--add-host`, has been added to `docker run` that lets you add +lines to `/etc/hosts`. This allows you to specify different name resolution for +the container than it would get via DNS. For more information, see the [command +line reference](/reference/commandline/cli/#run). + +* You can now set a `DOCKER_TLS_VERIFY` environment variable to secure +connections by default (rather than having to pass the `--tlsverify` flag on +every call). For more information, see the [https guide](/articles/https). + +* Three security issues have been addressed in this release: [CVE-2014-5280, +CVE-2014-5270, and +CVE-2014-5282](https://groups.google.com/forum/#!msg/docker-announce/aQoVmQlcE0A/smPuBNYf8VwJ). + +##Version 1.2.0 + +This version fixes a number of bugs and issues and adds new functions and other +improvements. These include: + +###New Features + +*New restart policies* + +We added a `--restart flag` to `docker run` to specify a restart policy for your +container. Currently, there are three policies available: + +* `no` – Do not restart the container if it dies. (default) * `on-failure` – +Restart the container if it exits with a non-zero exit code. This can also +accept an optional maximum restart count (e.g. `on-failure:5`). * `always` – +Always restart the container no matter what exit code is returned. This +deprecates the `--restart` flag on the Docker daemon. + +*New flags for `docker run`: `--cap-add` and `–-cap-drop`* + +In previous releases, Docker containers could either be given complete +capabilities or they could all follow a whitelist of allowed capabilities while +dropping all others. Further, using `--privileged` would grant all capabilities +inside a container, rather than applying a whitelist. This was not recommended +for production use because it’s really unsafe; it’s as if you were directly in +the host. + +This release introduces two new flags for `docker run`, `--cap-add` and +`--cap-drop`, that give you fine-grain control over the specific capabilities +you want grant to a particular container. + +*New `-–device` flag for `docker run`* + +Previously, you could only use devices inside your containers by bind mounting +them (with `-v`) in a `--privileged` container. With this release, we introduce +the `--device flag` to `docker run` which lets you use a device without +requiring a privileged container. + +*Writable `/etc/hosts`, `/etc/hostname` and `/etc/resolv.conf`* + +You can now edit `/etc/hosts`, `/etc/hostname` and `/etc/resolve.conf` in a +running container. This is useful if you need to install BIND or other services +that might override one of those files. + +Note, however, that changes to these files are not saved when running `docker +build` and so will not be preserved in the resulting image. The changes will +only “stick” in a running container. + +*Docker proxy in a separate process* + +The Docker userland proxy that routes outbound traffic to your containers now +has its own separate process (one process per connection). This greatly reduces +the load on the daemon, which increases stability and efficiency. + +###Other improvements & changes + +* When using `docker rm -f`, Docker now kills the container (instead of stopping +it) before removing it . If you intend to stop the container cleanly, you can +use `docker stop`. + +* Added support for IPv6 addresses in `--dns` + +* Added search capability in private registries + +##Version 1.1.0 + +###New Features + +*`.dockerignore` support* + +You can now add a `.dockerignore` file next to your `Dockerfile` and Docker will +ignore files and directories specified in that file when sending the build +context to the daemon. Example: +https://github.com/docker/docker/blob/master/.dockerignore + +*Pause containers during commit* + +Doing a commit on a running container was not recommended because you could end +up with files in an inconsistent state (for example, if they were being written +during the commit). Containers are now paused when a commit is made to them. You +can disable this feature by doing a `docker commit --pause=false ` + +*Tailing logs* + +You can now tail the logs of a container. For example, you can get the last ten +lines of a log by using `docker logs --tail 10 `. You can also +follow the logs of a container without having to read the whole log file with +`docker logs --tail 0 -f `. + +*Allow a tar file as context for docker build* + +You can now pass a tar archive to `docker build` as context. This can be used to +automate docker builds, for example: `cat context.tar | docker build -` or +`docker run builder_image | docker build -` + +*Bind mounting your whole filesystem in a container* + +`/` is now allowed as source of `--volumes`. This means you can bind-mount your +whole system in a container if you need to. For example: `docker run -v +/:/my_host ubuntu:ro ls /my_host`. However, it is now forbidden to mount to /. + + +###Other Improvements & Changes + +* Port allocation has been improved. In the previous release, Docker could +prevent you from starting a container with previously allocated ports which +seemed to be in use when in fact they were not. This has been fixed. + +* A bug in `docker save` was introduced in the last release. The `docker save` +command could produce images with invalid metadata. The command now produces +images with correct metadata. + +* Running `docker inspect` in a container now returns which containers it is +linked to. + +* Parsing of the `docker commit` flag has improved validation, to better prevent +you from committing an image with a name such as `-m`. Image names with dashes +in them potentially conflict with command line flags. + +* The API now has Improved status codes for `start` and `stop`. Trying to start +a running container will now return a 304 error. + +* Performance has been improved overall. Starting the daemon is faster than in +previous releases. The daemon’s performance has also been improved when it is +working with large numbers of images and containers. + +* Fixed an issue with white-spaces and multi-lines in Dockerfiles. + +##Version 1.1.0 + +###New Features + +*`.dockerignore` support* + +You can now add a `.dockerignore` file next to your `Dockerfile` and Docker will +ignore files and directories specified in that file when sending the build +context to the daemon. Example: +https://github.com/dotcloud/docker/blob/master/.dockerignore + +*Pause containers during commit* + +Doing a commit on a running container was not recommended because you could end +up with files in an inconsistent state (for example, if they were being written +during the commit). Containers are now paused when a commit is made to them. You +can disable this feature by doing a `docker commit --pause=false ` + +*Tailing logs* + +You can now tail the logs of a container. For example, you can get the last ten +lines of a log by using `docker logs --tail 10 `. You can also +follow the logs of a container without having to read the whole log file with +`docker logs --tail 0 -f `. + +*Allow a tar file as context for docker build* + +You can now pass a tar archive to `docker build` as context. This can be used to +automate docker builds, for example: `cat context.tar | docker build -` or +`docker run builder_image | docker build -` + +*Bind mounting your whole filesystem in a container* + +`/` is now allowed as source of `--volumes`. This means you can bind-mount your +whole system in a container if you need to. For example: `docker run -v +/:/my_host ubuntu:ro ls /my_host`. However, it is now forbidden to mount to /. + + +###Other Improvements & Changes + +* Port allocation has been improved. In the previous release, Docker could +prevent you from starting a container with previously allocated ports which +seemed to be in use when in fact they were not. This has been fixed. + +* A bug in `docker save` was introduced in the last release. The `docker save` +command could produce images with invalid metadata. The command now produces +images with correct metadata. + +* Running `docker inspect` in a container now returns which containers it is +linked to. + +* Parsing of the `docker commit` flag has improved validation, to better prevent +you from committing an image with a name such as `-m`. Image names with dashes +in them potentially conflict with command line flags. + +* The API now has Improved status codes for `start` and `stop`. Trying to start +a running container will now return a 304 error. + +* Performance has been improved overall. Starting the daemon is faster than in +previous releases. The daemon’s performance has also been improved when it is +working with large numbers of images and containers. + +* Fixed an issue with white-spaces and multi-lines in Dockerfiles. + +##Version 1.0.0 + +First production-ready release. Prior development history can be found by +searching in [GitHub](https://github.com/docker/docker). From 7f8cdeb18ba14502845efa7e7e90e913d2df23d2 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Fri, 24 Oct 2014 00:23:25 +0000 Subject: [PATCH 14/19] builder: some small fixups + fix a bug where empty entrypoints would not override inheritance. Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) --- builder/dispatchers.go | 10 +++--- builder/evaluator.go | 2 +- builder/parser/parser.go | 5 +-- integration-cli/docker_cli_build_test.go | 43 ++++++++++++++++++++++++ runconfig/merge.go | 5 ++- 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/builder/dispatchers.go b/builder/dispatchers.go index 0c2a580872..e585c4021e 100644 --- a/builder/dispatchers.go +++ b/builder/dispatchers.go @@ -195,7 +195,7 @@ func run(b *Builder, args []string, attributes map[string]bool, original string) defer func(cmd []string) { b.Config.Cmd = cmd }(cmd) - log.Debugf("Command to be executed: %v", b.Config.Cmd) + log.Debugf("[BUILDER] Command to be executed: %v", b.Config.Cmd) hit, err := b.probeCache() if err != nil { @@ -261,14 +261,14 @@ func entrypoint(b *Builder, args []string, attributes map[string]bool, original parsed := handleJsonArgs(args, attributes) switch { - case len(parsed) == 0: - // ENTYRPOINT [] - b.Config.Entrypoint = nil case attributes["json"]: // ENTRYPOINT ["echo", "hi"] b.Config.Entrypoint = parsed + case len(parsed) == 0: + // ENTRYPOINT [] + b.Config.Entrypoint = nil default: - // ENTYRPOINT echo hi + // ENTRYPOINT echo hi b.Config.Entrypoint = []string{"/bin/sh", "-c", parsed[0]} } diff --git a/builder/evaluator.go b/builder/evaluator.go index 4122616350..aed8d29335 100644 --- a/builder/evaluator.go +++ b/builder/evaluator.go @@ -149,7 +149,7 @@ func (b *Builder) Run(context io.Reader) (string, error) { b.dockerfile = ast // some initializations that would not have been supplied by the caller. - b.Config = &runconfig.Config{Entrypoint: []string{}, Cmd: nil} + b.Config = &runconfig.Config{} b.TmpContainers = map[string]struct{}{} for i, n := range b.dockerfile.Children { diff --git a/builder/parser/parser.go b/builder/parser/parser.go index 5e8bcb5a9c..6b0ab7ab8c 100644 --- a/builder/parser/parser.go +++ b/builder/parser/parser.go @@ -87,10 +87,11 @@ func parseLine(line string) (string, *Node, error) { if sexp.Value != "" || sexp.Next != nil || sexp.Children != nil { node.Next = sexp - node.Attributes = attrs - node.Original = line } + node.Attributes = attrs + node.Original = line + return "", node, nil } diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 808f64c7e9..8be0e01a26 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -1362,6 +1362,49 @@ func TestBuildExpose(t *testing.T) { logDone("build - expose") } +func TestBuildEmptyEntrypointInheritance(t *testing.T) { + name := "testbuildentrypointinheritance" + name2 := "testbuildentrypointinheritance2" + defer deleteImages(name, name2) + + _, err := buildImage(name, + `FROM busybox + ENTRYPOINT ["/bin/echo"]`, + true) + if err != nil { + t.Fatal(err) + } + res, err := inspectField(name, "Config.Entrypoint") + if err != nil { + t.Fatal(err) + } + + expected := "[/bin/echo]" + if res != expected { + t.Fatalf("Entrypoint %s, expected %s", res, expected) + } + + _, err = buildImage(name2, + fmt.Sprintf(`FROM %s + ENTRYPOINT []`, name), + true) + if err != nil { + t.Fatal(err) + } + res, err = inspectField(name2, "Config.Entrypoint") + if err != nil { + t.Fatal(err) + } + + expected = "[]" + + if res != expected { + t.Fatalf("Entrypoint %s, expected %s", res, expected) + } + + logDone("build - empty entrypoint inheritance") +} + func TestBuildEmptyEntrypoint(t *testing.T) { name := "testbuildentrypoint" defer deleteImages(name) diff --git a/runconfig/merge.go b/runconfig/merge.go index 0c60d1df0b..64950bf625 100644 --- a/runconfig/merge.go +++ b/runconfig/merge.go @@ -88,7 +88,10 @@ func Merge(userConf, imageConf *Config) error { if len(userConf.Cmd) == 0 { userConf.Cmd = imageConf.Cmd } - userConf.Entrypoint = imageConf.Entrypoint + + if userConf.Entrypoint == nil { + userConf.Entrypoint = imageConf.Entrypoint + } } if userConf.WorkingDir == "" { userConf.WorkingDir = imageConf.WorkingDir From 2dac82eb82b469f94ef26a153d2679663b048ad3 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Sat, 25 Oct 2014 17:58:57 +0000 Subject: [PATCH 15/19] builder: handle escapes without swallowing all of them. Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) --- builder/support.go | 3 +- integration-cli/docker_cli_build_test.go | 86 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/builder/support.go b/builder/support.go index 6c7ac4096e..6833457f3a 100644 --- a/builder/support.go +++ b/builder/support.go @@ -24,8 +24,9 @@ func (b *Builder) replaceEnv(str string) string { continue } + prefix := match[:idx] stripped := match[idx+2:] - str = strings.Replace(str, match, "$"+stripped, -1) + str = strings.Replace(str, match, prefix+"$"+stripped, -1) continue } diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 8be0e01a26..a4922446c9 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -15,6 +15,92 @@ import ( "github.com/docker/docker/pkg/archive" ) +func TestBuildHandleEscapes(t *testing.T) { + name := "testbuildhandleescapes" + + defer deleteImages(name) + + _, err := buildImage(name, + ` + FROM scratch + ENV FOO bar + VOLUME ${FOO} + `, true) + + if err != nil { + t.Fatal(err) + } + + var result map[string]map[string]struct{} + + res, err := inspectFieldJSON(name, "Config.Volumes") + if err != nil { + t.Fatal(err) + } + + if err = unmarshalJSON([]byte(res), &result); err != nil { + t.Fatal(err) + } + + if _, ok := result["bar"]; !ok { + t.Fatal("Could not find volume bar set from env foo in volumes table") + } + + _, err = buildImage(name, + ` + FROM scratch + ENV FOO bar + VOLUME \${FOO} + `, true) + + if err != nil { + t.Fatal(err) + } + + res, err = inspectFieldJSON(name, "Config.Volumes") + if err != nil { + t.Fatal(err) + } + + if err = unmarshalJSON([]byte(res), &result); err != nil { + t.Fatal(err) + } + + if _, ok := result["${FOO}"]; !ok { + t.Fatal("Could not find volume ${FOO} set from env foo in volumes table") + } + + // this test in particular provides *7* backslashes and expects 6 to come back. + // Like above, the first escape is swallowed and the rest are treated as + // literals, this one is just less obvious because of all the character noise. + + _, err = buildImage(name, + ` + FROM scratch + ENV FOO bar + VOLUME \\\\\\\${FOO} + `, true) + + if err != nil { + t.Fatal(err) + } + + res, err = inspectFieldJSON(name, "Config.Volumes") + if err != nil { + t.Fatal(err) + } + + if err = unmarshalJSON([]byte(res), &result); err != nil { + t.Fatal(err) + } + + if _, ok := result[`\\\\\\${FOO}`]; !ok { + t.Fatal(`Could not find volume \\\\\\${FOO} set from env foo in volumes table`) + } + + logDone("build - handle escapes") +} + func TestBuildOnBuildLowercase(t *testing.T) { name := "testbuildonbuildlowercase" name2 := "testbuildonbuildlowercase2" From 463297ffe9d0b671d3b26e13905855ab3ff85d57 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Sat, 25 Oct 2014 18:29:18 +0000 Subject: [PATCH 16/19] builder: whitelist verbs useful for environment replacement. Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) --- builder/evaluator.go | 20 ++- integration-cli/docker_cli_build_test.go | 184 ++++++++++++++++++++++- 2 files changed, 201 insertions(+), 3 deletions(-) diff --git a/builder/evaluator.go b/builder/evaluator.go index aed8d29335..7884d36ac2 100644 --- a/builder/evaluator.go +++ b/builder/evaluator.go @@ -41,6 +41,17 @@ var ( ErrDockerfileEmpty = errors.New("Dockerfile cannot be empty") ) +// Environment variable interpolation will happen on these statements only. +var replaceEnvAllowed = map[string]struct{}{ + "env": {}, + "add": {}, + "copy": {}, + "workdir": {}, + "expose": {}, + "volume": {}, + "user": {}, +} + var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) error func init() { @@ -196,13 +207,18 @@ func (b *Builder) dispatch(stepN int, ast *parser.Node) error { if cmd == "onbuild" { ast = ast.Next.Children[0] - strs = append(strs, b.replaceEnv(ast.Value)) + strs = append(strs, ast.Value) msg += " " + ast.Value } for ast.Next != nil { ast = ast.Next - strs = append(strs, b.replaceEnv(ast.Value)) + var str string + str = ast.Value + if _, ok := replaceEnvAllowed[cmd]; ok { + str = b.replaceEnv(ast.Value) + } + strs = append(strs, str) msg += " " + ast.Value } diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index a4922446c9..276a16f299 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -2,6 +2,7 @@ package main import ( "archive/tar" + "encoding/json" "fmt" "io/ioutil" "os" @@ -15,6 +16,186 @@ import ( "github.com/docker/docker/pkg/archive" ) +func TestBuildEnvironmentReplacementUser(t *testing.T) { + name := "testbuildenvironmentreplacement" + defer deleteImages(name) + + _, err := buildImage(name, ` + FROM scratch + ENV user foo + USER ${user} + `, true) + if err != nil { + t.Fatal(err) + } + + res, err := inspectFieldJSON(name, "Config.User") + if err != nil { + t.Fatal(err) + } + + if res != `"foo"` { + t.Fatal("User foo from environment not in Config.User on image") + } + + logDone("build - user environment replacement") +} + +func TestBuildEnvironmentReplacementVolume(t *testing.T) { + name := "testbuildenvironmentreplacement" + defer deleteImages(name) + + _, err := buildImage(name, ` + FROM scratch + ENV volume /quux + VOLUME ${volume} + `, true) + if err != nil { + t.Fatal(err) + } + + res, err := inspectFieldJSON(name, "Config.Volumes") + if err != nil { + t.Fatal(err) + } + + var volumes map[string]interface{} + + if err := json.Unmarshal([]byte(res), &volumes); err != nil { + t.Fatal(err) + } + + if _, ok := volumes["/quux"]; !ok { + t.Fatal("Volume /quux from environment not in Config.Volumes on image") + } + + logDone("build - volume environment replacement") +} + +func TestBuildEnvironmentReplacementExpose(t *testing.T) { + name := "testbuildenvironmentreplacement" + defer deleteImages(name) + + _, err := buildImage(name, ` + FROM scratch + ENV port 80 + EXPOSE ${port} + `, true) + if err != nil { + t.Fatal(err) + } + + res, err := inspectFieldJSON(name, "Config.ExposedPorts") + if err != nil { + t.Fatal(err) + } + + var exposedPorts map[string]interface{} + + if err := json.Unmarshal([]byte(res), &exposedPorts); err != nil { + t.Fatal(err) + } + + if _, ok := exposedPorts["80/tcp"]; !ok { + t.Fatal("Exposed port 80 from environment not in Config.ExposedPorts on image") + } + + logDone("build - expose environment replacement") +} + +func TestBuildEnvironmentReplacementWorkdir(t *testing.T) { + name := "testbuildenvironmentreplacement" + defer deleteImages(name) + + _, err := buildImage(name, ` + FROM busybox + ENV MYWORKDIR /work + RUN mkdir ${MYWORKDIR} + WORKDIR ${MYWORKDIR} + `, true) + + if err != nil { + t.Fatal(err) + } + + logDone("build - workdir environment replacement") +} + +func TestBuildEnvironmentReplacementAddCopy(t *testing.T) { + name := "testbuildenvironmentreplacement" + defer deleteImages(name) + + ctx, err := fakeContext(` + FROM scratch + ENV baz foo + ENV quux bar + ENV dot . + + ADD ${baz} ${dot} + COPY ${quux} ${dot} + `, + map[string]string{ + "foo": "test1", + "bar": "test2", + }) + + if err != nil { + t.Fatal(err) + } + + if _, err := buildImageFromContext(name, ctx, true); err != nil { + t.Fatal(err) + } + + logDone("build - add/copy environment replacement") +} + +func TestBuildEnvironmentReplacementEnv(t *testing.T) { + name := "testbuildenvironmentreplacement" + + defer deleteImages(name) + + _, err := buildImage(name, + ` + FROM scratch + ENV foo foo + ENV bar ${foo} + `, true) + + if err != nil { + t.Fatal(err) + } + + res, err := inspectFieldJSON(name, "Config.Env") + if err != nil { + t.Fatal(err) + } + + envResult := []string{} + + if err = unmarshalJSON([]byte(res), &envResult); err != nil { + t.Fatal(err) + } + + found := false + + for _, env := range envResult { + parts := strings.SplitN(env, "=", 2) + if parts[0] == "bar" { + found = true + if parts[1] != "foo" { + t.Fatal("Could not find replaced var for env `bar`: got %q instead of `foo`", parts[1]) + } + } + } + + if !found { + t.Fatal("Never found the `bar` env variable") + } + + logDone("build - env environment replacement") +} + func TestBuildHandleEscapes(t *testing.T) { name := "testbuildhandleescapes" @@ -170,7 +351,7 @@ func TestBuildEnvOverwrite(t *testing.T) { ` FROM busybox ENV TEST foo - CMD echo \${TEST} + CMD echo ${TEST} `, true) @@ -2547,6 +2728,7 @@ func TestBuildEnvUsage(t *testing.T) { name := "testbuildenvusage" defer deleteImages(name) dockerfile := `FROM busybox +ENV HOME /root ENV PATH $HOME/bin:$PATH ENV PATH /tmp:$PATH RUN [ "$PATH" = "/tmp:$HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ] From 9fc8b7f4e1f88d8573118b7300b18379072418d1 Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Mon, 27 Oct 2014 21:15:28 +0000 Subject: [PATCH 17/19] builder: Restore /bin/sh handling in CMD when entrypoint is specified with JSON Docker-DCO-1.1-Signed-off-by: Erik Hollensbe (github: erikh) --- builder/dispatchers.go | 2 +- integration-cli/docker_cli_build_test.go | 34 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/builder/dispatchers.go b/builder/dispatchers.go index e585c4021e..2184e48a81 100644 --- a/builder/dispatchers.go +++ b/builder/dispatchers.go @@ -234,7 +234,7 @@ func run(b *Builder, args []string, attributes map[string]bool, original string) func cmd(b *Builder, args []string, attributes map[string]bool, original string) error { b.Config.Cmd = handleJsonArgs(args, attributes) - if !attributes["json"] && len(b.Config.Entrypoint) == 0 { + if !attributes["json"] { b.Config.Cmd = append([]string{"/bin/sh", "-c"}, b.Config.Cmd...) } diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 276a16f299..0885f9131a 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -16,6 +16,40 @@ import ( "github.com/docker/docker/pkg/archive" ) +func TestBuildShCmdJSONEntrypoint(t *testing.T) { + name := "testbuildshcmdjsonentrypoint" + defer deleteImages(name) + + _, err := buildImage( + name, + ` + FROM busybox + ENTRYPOINT ["/bin/echo"] + CMD echo test + `, + true) + + if err != nil { + t.Fatal(err) + } + + out, _, err := runCommandWithOutput( + exec.Command( + dockerBinary, + "run", + name)) + + if err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(out) != "/bin/sh -c echo test" { + t.Fatal("CMD did not contain /bin/sh -c") + } + + logDone("build - CMD should always contain /bin/sh -c when specified without JSON") +} + func TestBuildEnvironmentReplacementUser(t *testing.T) { name := "testbuildenvironmentreplacement" defer deleteImages(name) From e6efbd659606386db4d0b83b98f9e189cf42595c Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Tue, 28 Oct 2014 21:20:30 -0400 Subject: [PATCH 18/19] Fix login command Signed-off-by: Tibor Vass --- registry/service.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/registry/service.go b/registry/service.go index 32274f407d..7051d93430 100644 --- a/registry/service.go +++ b/registry/service.go @@ -50,9 +50,11 @@ func (s *Service) Auth(job *engine.Job) engine.Status { authConfig.ServerAddress = endpoint.String() } - if _, err := Login(authConfig, HTTPRequestFactory(nil)); err != nil { + status, err := Login(authConfig, HTTPRequestFactory(nil)) + if err != nil { return job.Error(err) } + job.Printf("%s\n", status) return engine.StatusOK } From 4e9bbfa90054cd730e81b53b2de67a74306afc95 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Mon, 20 Oct 2014 19:17:32 -0400 Subject: [PATCH 19/19] Bump to version v1.3.1 Signed-off-by: Tibor Vass --- CHANGELOG.md | 18 ++++++++++++++++++ VERSION | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5218eea2..f958bbd48c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 1.3.1 (2014-10-28) + +#### Security +* Prevent fallback to SSL protocols < TLS 1.0 for client, daemon and registry ++ Secure HTTPS connection to registries with certificate verification and without HTTP fallback unless `--insecure-registry` is specified + +#### Runtime +- Fix issue where volumes would not be shared + +#### Client +- Fix issue with `--iptables=false` not automatically setting `--ip-masq=false` +- Fix docker run output to non-TTY stdout + +#### Builder +- Fix escaping `$` for environment variables +- Fix issue with lowercase `onbuild` Dockerfile instruction +- Restrict envrionment variable expansion to `ENV`, `ADD`, `COPY`, `WORKDIR`, `EXPOSE`, `VOLUME` and `USER` + ## 1.3.0 (2014-10-14) #### Notable features since 1.2.0 diff --git a/VERSION b/VERSION index f0bb29e763..3a3cd8cc8b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0 +1.3.1