From f4aa84a057f7d517cd91c3ede666029029ec88ce Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Mon, 20 Oct 2014 02:29:59 -0700 Subject: [PATCH 1/7] updated Docker Client to accept TLS --- shared/build/build.go | 2 +- shared/build/docker/client.go | 104 +++++++++++++++++++++++++++------- 2 files changed, 86 insertions(+), 20 deletions(-) diff --git a/shared/build/build.go b/shared/build/build.go index 1f0084e69..986805851 100644 --- a/shared/build/build.go +++ b/shared/build/build.go @@ -262,7 +262,7 @@ func (b *Builder) setup() error { if err != nil { // if we have problems with the image make sure // we remove it before we exit - b.dockerClient.Images.Remove(id) + log.Errf("failed to verify build image %s", id) return err } diff --git a/shared/build/docker/client.go b/shared/build/docker/client.go index 4f2eefa1b..919c933e5 100644 --- a/shared/build/docker/client.go +++ b/shared/build/docker/client.go @@ -2,6 +2,7 @@ package docker import ( "bytes" + "crypto/tls" "encoding/json" "errors" "fmt" @@ -12,6 +13,7 @@ import ( "net/http/httputil" "os" "strings" + "time" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/term" @@ -58,7 +60,45 @@ func NewHost(address string) *Client { return c } +func NewClient(addr, cert, key string) (*Client, error) { + // generate a new Client + var cli = NewHost(addr) + cli.tls = new(tls.Config) + + // this is required in order for Docker to connect + // to a certificate generated for an IP address and + // not a Domain name + cli.tls.InsecureSkipVerify = true + + // loads the keyvalue pair and stores the + // cert (pem) in a certificate store (array) + pem, err := tls.LoadX509KeyPair(cert, key) + if err != nil { + return nil, err + } + cli.tls.Certificates = []tls.Certificate{pem} + + // creates a transport that uses the custom tls + // configuration to securely connect to remote + // Docker clients. + cli.trans = &http.Transport{ + TLSClientConfig: cli.tls, + Dial: func(dial_network, dial_addr string) (net.Conn, error) { + return net.DialTimeout(cli.proto, cli.addr, 32*time.Second) + }, + } + + if cli.proto == "unix" { + // no need in compressing for local communications + cli.trans.DisableCompression = true + } + + return cli, nil +} + type Client struct { + tls *tls.Config + trans *http.Transport proto string addr string @@ -133,16 +173,10 @@ func (c *Client) do(method, path string, in, out interface{}) error { req.Header.Set("Content-Type", "application/json") // dial the host server - req.Host = c.addr - dial, err := net.Dial(c.proto, c.addr) - if err != nil { - return err - } + req.URL.Host = c.addr + req.URL.Scheme = "http" - // make the request - conn := httputil.NewClientConn(dial, nil) - resp, err := conn.Do(req) - defer conn.Close() + resp, err := c.HTTPClient().Do(req) if err != nil { return err } @@ -184,7 +218,7 @@ func (c *Client) hijack(method, path string, setRawTerminal bool, out io.Writer) req.Header.Set("Content-Type", "plain/text") req.Host = c.addr - dial, err := net.Dial(c.proto, c.addr) + dial, err := c.Dial() if err != nil { if strings.Contains(err.Error(), "connection refused") { return fmt.Errorf("Can't connect to docker daemon. Is 'docker -d' running on this host?") @@ -239,16 +273,27 @@ func (c *Client) stream(method, path string, in io.Reader, out io.Writer, header req.Header.Set("Content-Type", "plain/text") // dial the host server - req.Host = c.addr - dial, err := net.Dial(c.proto, c.addr) - if err != nil { - return err - } + /* + req.Host = c.addr + dial, err := net.Dial(c.proto, c.addr) + if err != nil { + return err + } - // make the request - conn := httputil.NewClientConn(dial, nil) - resp, err := conn.Do(req) - defer conn.Close() + // make the request + conn := httputil.NewClientConn(dial, nil) + resp, err := conn.Do(req) + defer conn.Close() + if err != nil { + return err + } + */ + + // dial the host server + req.URL.Host = c.addr + req.URL.Scheme = "http" + + resp, err := c.HTTPClient().Do(req) if err != nil { return err } @@ -270,6 +315,7 @@ func (c *Client) stream(method, path string, in io.Reader, out io.Writer, header // If no output we exit now with no errors if out == nil { + io.Copy(ioutil.Discard, resp.Body) return nil } @@ -289,3 +335,23 @@ func (c *Client) stream(method, path string, in io.Reader, out io.Writer, header return nil } + +func (c *Client) HTTPClient() *http.Client { + if c.trans != nil { + return &http.Client{Transport: c.trans} + } + return &http.Client{ + Transport: &http.Transport{ + Dial: func(dial_network, dial_addr string) (net.Conn, error) { + return net.DialTimeout(c.proto, c.addr, 32*time.Second) + }, + }, + } +} + +func (c *Client) Dial() (net.Conn, error) { + if c.tls != nil && c.proto != "unix" { + return tls.Dial(c.proto, c.addr, c.tls) + } + return net.Dial(c.proto, c.addr) +} From 505ebe70247cf2f473539bd8606ef1cc4b32f4fb Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Mon, 20 Oct 2014 22:52:34 -0700 Subject: [PATCH 2/7] removed hard-coded `http` in Docker client --- shared/build/docker/client.go | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/shared/build/docker/client.go b/shared/build/docker/client.go index 919c933e5..ea5ef08eb 100644 --- a/shared/build/docker/client.go +++ b/shared/build/docker/client.go @@ -175,6 +175,9 @@ func (c *Client) do(method, path string, in, out interface{}) error { // dial the host server req.URL.Host = c.addr req.URL.Scheme = "http" + if c.tls != nil { + req.URL.Scheme = "https" + } resp, err := c.HTTPClient().Do(req) if err != nil { @@ -272,26 +275,12 @@ func (c *Client) stream(method, path string, in io.Reader, out io.Writer, header req.Header.Set("User-Agent", "Docker-Client/0.6.4") req.Header.Set("Content-Type", "plain/text") - // dial the host server - /* - req.Host = c.addr - dial, err := net.Dial(c.proto, c.addr) - if err != nil { - return err - } - - // make the request - conn := httputil.NewClientConn(dial, nil) - resp, err := conn.Do(req) - defer conn.Close() - if err != nil { - return err - } - */ - // dial the host server req.URL.Host = c.addr req.URL.Scheme = "http" + if c.tls != nil { + req.URL.Scheme = "https" + } resp, err := c.HTTPClient().Do(req) if err != nil { From 7dfc67113dfd7c2df6a90bf2f5118084bf39d326 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Tue, 21 Oct 2014 00:38:19 -0700 Subject: [PATCH 3/7] cleanup docker.New to overload, so to speak --- shared/build/docker/client.go | 123 ++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 43 deletions(-) diff --git a/shared/build/docker/client.go b/shared/build/docker/client.go index ea5ef08eb..a9072544f 100644 --- a/shared/build/docker/client.go +++ b/shared/build/docker/client.go @@ -34,53 +34,52 @@ var Logging = true // New creates an instance of the Docker Client func New() *Client { - c := &Client{} - - c.setHost(DEFAULTUNIXSOCKET) - - c.Images = &ImageService{c} - c.Containers = &ContainerService{c} - return c + return NewHost("") } func NewHost(address string) *Client { - c := &Client{} - - // parse the address and split - pieces := strings.Split(address, "://") - if len(pieces) == 2 { - c.proto = pieces[0] - c.addr = pieces[1] - } else if len(pieces) == 1 { - c.addr = pieces[0] - } - - c.Images = &ImageService{c} - c.Containers = &ContainerService{c} - return c + var cli, _ = NewClient(address, "", "") + return cli } -func NewClient(addr, cert, key string) (*Client, error) { - // generate a new Client - var cli = NewHost(addr) - cli.tls = new(tls.Config) +func NewClient(uri, cert, key string) (*Client, error) { + var host = GetHost(uri) + var proto, addr = GetProtoAddr(host) - // this is required in order for Docker to connect - // to a certificate generated for an IP address and - // not a Domain name - cli.tls.InsecureSkipVerify = true + var cli = new(Client) + cli.proto = proto + cli.addr = addr + cli.scheme = "http" + cli.Images = &ImageService{cli} + cli.Containers = &ContainerService{cli} - // loads the keyvalue pair and stores the - // cert (pem) in a certificate store (array) + // if no certificate is provided returns the + // client with no TLS configured. + if len(cert) == 0 || len(key) == 0 { + return cli, nil + } + + // loads the key value pair in pem format pem, err := tls.LoadX509KeyPair(cert, key) if err != nil { return nil, err } + + // setup the client TLS and store the certificate. + // also skip verification since we are (typically) + // going to be using certs for IP addresses. + cli.scheme = "https" + cli.tls = new(tls.Config) + cli.tls.InsecureSkipVerify = true cli.tls.Certificates = []tls.Certificate{pem} - // creates a transport that uses the custom tls - // configuration to securely connect to remote - // Docker clients. + // disable compression for local socket communication. + if cli.proto == DEFAULTPROTOCOL { + cli.trans.DisableCompression = true + } + + // creates a transport that uses the custom tls configuration + // to securely connect to remote Docker clients. cli.trans = &http.Transport{ TLSClientConfig: cli.tls, Dial: func(dial_network, dial_addr string) (net.Conn, error) { @@ -88,19 +87,15 @@ func NewClient(addr, cert, key string) (*Client, error) { }, } - if cli.proto == "unix" { - // no need in compressing for local communications - cli.trans.DisableCompression = true - } - return cli, nil } type Client struct { - tls *tls.Config - trans *http.Transport - proto string - addr string + tls *tls.Config + trans *http.Transport + scheme string + proto string + addr string Images *ImageService Containers *ContainerService @@ -149,6 +144,48 @@ func (c *Client) setHost(defaultUnixSocket string) { } } +// GetHost returns the Docker Host address in order to +// connect to the Docker Daemon. It implements a very +// simple set of fallthrough logic to determine which +// address to use. +func GetHost(host string) string { + // if a default value was provided this + // shoudl be used + if len(host) != 0 { + return host + } + // else attempt to use the DOCKER_HOST + // environment variable + var env = os.Getenv("DOCKER_HOST") + if len(env) != 0 { + return env + } + // else check to see if the default unix + // socket exists and return + _, err := os.Stat(DEFAULTUNIXSOCKET) + if err == nil { + return fmt.Sprintf("%s://%s", DEFAULTPROTOCOL, DEFAULTUNIXSOCKET) + } + // else return the standard TCP address + return fmt.Sprintf("tcp://0.0.0.0:%d", DEFAULTHTTPPORT) +} + +// GetProtoAddr is a helper function that splits +// a host into Protocol and Address. +func GetProtoAddr(host string) (string, string) { + var parts = strings.Split(host, "://") + var proto, addr string + switch { + case len(parts) == 2: + proto = parts[0] + addr = parts[1] + default: + proto = "tcp" + addr = parts[0] + } + return proto, addr +} + // helper function used to make HTTP requests to the Docker daemon. func (c *Client) do(method, path string, in, out interface{}) error { // if data input is provided, serialize to JSON From 8436bd027031efe9bf9f7d5c2ac843503d68c4de Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Tue, 21 Oct 2014 00:52:35 -0700 Subject: [PATCH 4/7] minor docker client refactoring --- shared/build/docker/client.go | 108 ++++++++++++----------------- shared/build/docker/client_test.go | 35 ---------- 2 files changed, 43 insertions(+), 100 deletions(-) diff --git a/shared/build/docker/client.go b/shared/build/docker/client.go index a9072544f..271e9fe79 100644 --- a/shared/build/docker/client.go +++ b/shared/build/docker/client.go @@ -44,7 +44,7 @@ func NewHost(address string) *Client { func NewClient(uri, cert, key string) (*Client, error) { var host = GetHost(uri) - var proto, addr = GetProtoAddr(host) + var proto, addr = SplitProtoAddr(host) var cli = new(Client) cli.proto = proto @@ -90,6 +90,48 @@ func NewClient(uri, cert, key string) (*Client, error) { return cli, nil } +// GetHost returns the Docker Host address in order to +// connect to the Docker Daemon. It implements a very +// simple set of fallthrough logic to determine which +// address to use. +func GetHost(host string) string { + // if a default value was provided this + // shoudl be used + if len(host) != 0 { + return host + } + // else attempt to use the DOCKER_HOST + // environment variable + var env = os.Getenv("DOCKER_HOST") + if len(env) != 0 { + return env + } + // else check to see if the default unix + // socket exists and return + _, err := os.Stat(DEFAULTUNIXSOCKET) + if err == nil { + return fmt.Sprintf("%s://%s", DEFAULTPROTOCOL, DEFAULTUNIXSOCKET) + } + // else return the standard TCP address + return fmt.Sprintf("tcp://0.0.0.0:%d", DEFAULTHTTPPORT) +} + +// SplitProtoAddr is a helper function that splits +// a host into Protocol and Address. +func SplitProtoAddr(host string) (string, string) { + var parts = strings.Split(host, "://") + var proto, addr string + switch { + case len(parts) == 2: + proto = parts[0] + addr = parts[1] + default: + proto = "tcp" + addr = parts[0] + } + return proto, addr +} + type Client struct { tls *tls.Config trans *http.Transport @@ -122,70 +164,6 @@ var ( ErrBadRequest = errors.New("Bad Request") ) -func (c *Client) setHost(defaultUnixSocket string) { - c.proto = DEFAULTPROTOCOL - c.addr = defaultUnixSocket - - if os.Getenv("DOCKER_HOST") != "" { - pieces := strings.Split(os.Getenv("DOCKER_HOST"), "://") - if len(pieces) == 2 { - c.proto = pieces[0] - c.addr = pieces[1] - } else if len(pieces) == 1 { - c.addr = pieces[0] - } - } else { - // if the default socket doesn't exist then - // we'll try to connect to the default tcp address - if _, err := os.Stat(defaultUnixSocket); err != nil { - c.proto = "tcp" - c.addr = "0.0.0.0:2375" - } - } -} - -// GetHost returns the Docker Host address in order to -// connect to the Docker Daemon. It implements a very -// simple set of fallthrough logic to determine which -// address to use. -func GetHost(host string) string { - // if a default value was provided this - // shoudl be used - if len(host) != 0 { - return host - } - // else attempt to use the DOCKER_HOST - // environment variable - var env = os.Getenv("DOCKER_HOST") - if len(env) != 0 { - return env - } - // else check to see if the default unix - // socket exists and return - _, err := os.Stat(DEFAULTUNIXSOCKET) - if err == nil { - return fmt.Sprintf("%s://%s", DEFAULTPROTOCOL, DEFAULTUNIXSOCKET) - } - // else return the standard TCP address - return fmt.Sprintf("tcp://0.0.0.0:%d", DEFAULTHTTPPORT) -} - -// GetProtoAddr is a helper function that splits -// a host into Protocol and Address. -func GetProtoAddr(host string) (string, string) { - var parts = strings.Split(host, "://") - var proto, addr string - switch { - case len(parts) == 2: - proto = parts[0] - addr = parts[1] - default: - proto = "tcp" - addr = parts[0] - } - return proto, addr -} - // helper function used to make HTTP requests to the Docker daemon. func (c *Client) do(method, path string, in, out interface{}) error { // if data input is provided, serialize to JSON diff --git a/shared/build/docker/client_test.go b/shared/build/docker/client_test.go index efdd06a0a..b40421ec1 100644 --- a/shared/build/docker/client_test.go +++ b/shared/build/docker/client_test.go @@ -1,7 +1,6 @@ package docker import ( - "io/ioutil" "os" "testing" ) @@ -31,37 +30,3 @@ func TestInvalidHostFromEnv(t *testing.T) { t.Fail() } } - -func TestSocketHost(t *testing.T) { - // create temporary file to represent the docker socket - file, err := ioutil.TempFile("", "TestDefaultUnixHost") - if err != nil { - t.Fail() - } - file.Close() - defer os.Remove(file.Name()) - - client := &Client{} - client.setHost(file.Name()) - - if client.proto != "unix" { - t.Fail() - } - - if client.addr != file.Name() { - t.Fail() - } -} - -func TestDefaultTcpHost(t *testing.T) { - client := &Client{} - client.setHost("/tmp/missing_socket") - - if client.proto != "tcp" { - t.Fail() - } - - if client.addr != "0.0.0.0:2375" { - t.Fail() - } -} From 8c51215e55b45ee76cf863428ae709be25f15227 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Tue, 21 Oct 2014 01:17:12 -0700 Subject: [PATCH 5/7] pass docker cert and key as byte arrays or file paths --- cli/build.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/cli/build.go b/cli/build.go index e50be1aa3..0c5a3e1b5 100644 --- a/cli/build.go +++ b/cli/build.go @@ -41,6 +41,21 @@ func NewBuildCommand() cli.Command { Name: "publish", Usage: "runs drone build with publishing enabled", }, + cli.StringFlag{ + Name: "docker-host", + Value: "", + Usage: "docker daemon address", + }, + cli.StringFlag{ + Name: "docker-cert", + Value: "", + Usage: "docker daemon tls certificate", + }, + cli.StringFlag{ + Name: "docker-key", + Value: "", + Usage: "docker daemon tls key", + }, }, Action: func(c *cli.Context) { buildCommandFunc(c) @@ -56,6 +71,10 @@ func buildCommandFunc(c *cli.Context) { var publish = c.Bool("publish") var path string + var dockerhost = c.String("docker-host") + var dockercert = c.String("docker-cert") + var dockerkey = c.String("docker-key") + // the path is provided as an optional argument that // will otherwise default to $PWD/.drone.yml if len(c.Args()) > 0 { @@ -80,12 +99,17 @@ func buildCommandFunc(c *cli.Context) { log.SetPriority(log.LOG_DEBUG) //LOG_NOTICE docker.Logging = false - var exit, _ = run(path, identity, publish, deploy, privileged) + var exit, _ = run(path, identity, dockerhost, dockercert, dockerkey, publish, deploy, privileged) os.Exit(exit) } -func run(path, identity string, publish, deploy, privileged bool) (int, error) { - dockerClient := docker.New() +// TODO this has gotten a bit out of hand. refactor input params +func run(path, identity, dockerhost, dockercert, dockerkey string, publish, deploy, privileged bool) (int, error) { + dockerClient, err := docker.NewHostCertFile(dockerhost, dockercert, dockerkey) + if err != nil { + log.Err(err.Error()) + return EXIT_STATUS, err + } // parse the private environment variables envs := getParamMap("DRONE_ENV_") From 2c4e992f1fdf342bd723e0feb00e26af614d04b9 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Tue, 21 Oct 2014 01:17:33 -0700 Subject: [PATCH 6/7] drone client accepts docker host, cert and key --- shared/build/docker/client.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/shared/build/docker/client.go b/shared/build/docker/client.go index 271e9fe79..41fc48b7d 100644 --- a/shared/build/docker/client.go +++ b/shared/build/docker/client.go @@ -37,12 +37,27 @@ func New() *Client { return NewHost("") } -func NewHost(address string) *Client { - var cli, _ = NewClient(address, "", "") +func NewHost(uri string) *Client { + var cli, _ = NewHostCert(uri, nil, nil) return cli } -func NewClient(uri, cert, key string) (*Client, error) { +func NewHostCertFile(uri, cert, key string) (*Client, error) { + if len(key) == 0 || len(cert) == 0 { + return NewHostCert(uri, nil, nil) + } + certfile, err := ioutil.ReadFile(cert) + if err != nil { + return nil, err + } + keyfile, err := ioutil.ReadFile(key) + if err != nil { + return nil, err + } + return NewHostCert(uri, certfile, keyfile) +} + +func NewHostCert(uri string, cert, key []byte) (*Client, error) { var host = GetHost(uri) var proto, addr = SplitProtoAddr(host) @@ -55,12 +70,12 @@ func NewClient(uri, cert, key string) (*Client, error) { // if no certificate is provided returns the // client with no TLS configured. - if len(cert) == 0 || len(key) == 0 { + if cert == nil || key == nil || len(cert) == 0 || len(key) == 0 { return cli, nil } // loads the key value pair in pem format - pem, err := tls.LoadX509KeyPair(cert, key) + pem, err := tls.X509KeyPair(cert, key) if err != nil { return nil, err } From 3cd1631c05af329867d91349cbcaaf9960d814a3 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Wed, 22 Oct 2014 23:23:05 -0700 Subject: [PATCH 7/7] updated README --- README.md | 2 ++ packaging/root/etc/drone/drone.toml | 2 ++ server/main.go | 5 ++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aef24b81d..98dd4d143 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ user="" pass="" [worker] +cert="" +key="" nodes=[ "unix:///var/run/docker.sock", "unix:///var/run/docker.sock" diff --git a/packaging/root/etc/drone/drone.toml b/packaging/root/etc/drone/drone.toml index 06d17a9e7..a53f2e1f0 100644 --- a/packaging/root/etc/drone/drone.toml +++ b/packaging/root/etc/drone/drone.toml @@ -65,6 +65,8 @@ datasource="/var/lib/drone/drone.sqlite" # pass="" # [worker] +# cert="" +# key="" # nodes=[ # "unix:///var/run/docker.sock", # "unix:///var/run/docker.sock" diff --git a/server/main.go b/server/main.go index abdbc6788..e55e29dc6 100644 --- a/server/main.go +++ b/server/main.go @@ -60,9 +60,8 @@ var ( pub *pubsub.PubSub // Docker configuration details. - tlscacert = config.String("docker-tlscacert", "") - tlscert = config.String("docker-tlscert", "") - tlskey = config.String("docker-tlskey", "") + dockercrt = config.String("docker-cert", "") + dockerkey = config.String("docker-key", "") nodes StringArr db *sql.DB