diff --git a/docker/daemon/client.go b/docker/daemon/client.go new file mode 100644 index 0000000000..1628b9d27d --- /dev/null +++ b/docker/daemon/client.go @@ -0,0 +1,69 @@ +package daemon + +import ( + "github.com/containers/image/types" + dockerclient "github.com/docker/docker/client" + "github.com/docker/go-connections/tlsconfig" + "net/http" + "path/filepath" +) + +const ( + // The default API version to be used in case none is explicitly specified + defaultAPIVersion = "1.22" +) + +// NewDockerClient initializes a new API client based on the passed SystemContext. +func newDockerClient(ctx *types.SystemContext) (*dockerclient.Client, error) { + if ctx == nil { + return dockerclient.NewClient(dockerclient.DefaultDockerHost, defaultAPIVersion, nil, nil) + } + + httpClient, err := tlsConfig(ctx) + if err != nil { + return nil, err + } + + host := ctx.DockerDaemonHost + if host == "" { + host = dockerclient.DefaultDockerHost + } + + version := ctx.DockerDaemonAPIVersion + if version == "" { + version = defaultAPIVersion + } + + cli, err := dockerclient.NewClient(host, version, httpClient, nil) + if err != nil { + return cli, err + } + + return cli, nil +} + +func tlsConfig(ctx *types.SystemContext) (*http.Client, error) { + if ctx == nil || ctx.DockerDaemonCertPath == "" { + return nil, nil + } + + options := tlsconfig.Options{ + CAFile: filepath.Join(ctx.DockerDaemonCertPath, "ca.pem"), + CertFile: filepath.Join(ctx.DockerDaemonCertPath, "cert.pem"), + KeyFile: filepath.Join(ctx.DockerDaemonCertPath, "key.pem"), + InsecureSkipVerify: ctx.DockerDaemonInsecureSkipTLSVerify, + } + tlsc, err := tlsconfig.Client(options) + if err != nil { + return nil, err + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsc, + }, + CheckRedirect: dockerclient.CheckRedirect, + } + + return httpClient, nil +} diff --git a/docker/daemon/client_test.go b/docker/daemon/client_test.go new file mode 100644 index 0000000000..b7d031a544 --- /dev/null +++ b/docker/daemon/client_test.go @@ -0,0 +1,83 @@ +package daemon + +import "testing" +import ( + "github.com/containers/image/types" + dockerclient "github.com/docker/docker/client" + "github.com/stretchr/testify/assert" + "net/http" + "os" + "path/filepath" +) + +func TestDockerClientFromNilSystemContext(t *testing.T) { + client, err := newDockerClient(nil) + + assert.Nil(t, err, "There should be no error creating the Docker client") + assert.NotNil(t, client, "A Docker client reference should have been returned") + + assert.Equal(t, dockerclient.DefaultDockerHost, client.DaemonHost(), "The default docker host should have been used") + assert.Equal(t, defaultAPIVersion, client.ClientVersion(), "The default api version should have been used") +} + +func TestDockerClientFromCertContext(t *testing.T) { + testDir, err := os.Getwd() + if err != nil { + t.Fatal("Unable to determine the current test directory") + } + + host := "tcp://127.0.0.1:2376" + version := "1.11" + systemCtx := &types.SystemContext{ + DockerDaemonCertPath: filepath.Join(testDir, "testdata", "certs"), + DockerDaemonHost: host, + DockerDaemonAPIVersion: version, + DockerDaemonInsecureSkipTLSVerify: true, + } + + client, err := newDockerClient(systemCtx) + + assert.Nil(t, err, "There should be no error creating the Docker client") + assert.NotNil(t, client, "A Docker client reference should have been returned") + + assert.Equal(t, host, client.DaemonHost()) + assert.Equal(t, version, client.ClientVersion()) +} + +func TestTlsConfigFromNilSystemContext(t *testing.T) { + httpClient, err := tlsConfig(nil) + + assert.Nil(t, err, "There should be no error creating the HTTP client") + assert.Nil(t, httpClient, "With no explicit TLS configutation, there should be no HTTP client") +} + +func TestTlsConfigFromInvalidCertPath(t *testing.T) { + ctx := &types.SystemContext{ + DockerDaemonCertPath: "/foo/bar", + } + + _, err := tlsConfig(ctx) + + if assert.Error(t, err, "An error was expected") { + assert.Regexp(t, "could not read CA certificate", err.Error()) + } +} + +func TestTlsConfigFromCertPath(t *testing.T) { + testDir, err := os.Getwd() + if err != nil { + t.Fatal("Unable to determine the current test directory") + } + + ctx := &types.SystemContext{ + DockerDaemonCertPath: filepath.Join(testDir, "testdata", "certs"), + DockerDaemonInsecureSkipTLSVerify: true, + } + + httpClient, err := tlsConfig(ctx) + + assert.Nil(t, err, "There should be no error creating the HTTP client") + + transport := httpClient.Transport.(*http.Transport) + assert.True(t, transport.TLSClientConfig.InsecureSkipVerify, "TLS verification should be skipped") +} diff --git a/docker/daemon/daemon_dest.go b/docker/daemon/daemon_dest.go index 559e5c71df..4202b89f0c 100644 --- a/docker/daemon/daemon_dest.go +++ b/docker/daemon/daemon_dest.go @@ -24,7 +24,7 @@ type daemonImageDestination struct { } // newImageDestination returns a types.ImageDestination for the specified image reference. -func newImageDestination(systemCtx *types.SystemContext, ref daemonReference) (types.ImageDestination, error) { +func newImageDestination(ctx *types.SystemContext, ref daemonReference) (types.ImageDestination, error) { if ref.ref == nil { return nil, errors.Errorf("Invalid destination docker-daemon:%s: a destination must be a name:tag", ref.StringWithinTransport()) } @@ -33,7 +33,7 @@ func newImageDestination(systemCtx *types.SystemContext, ref daemonReference) (t return nil, errors.Errorf("Invalid destination docker-daemon:%s: a destination must be a name:tag", ref.StringWithinTransport()) } - c, err := client.NewClient(client.DefaultDockerHost, "1.22", nil, nil) // FIXME: overridable host + c, err := newDockerClient(ctx) if err != nil { return nil, errors.Wrap(err, "Error initializing docker engine client") } @@ -42,8 +42,8 @@ func newImageDestination(systemCtx *types.SystemContext, ref daemonReference) (t // Commit() may never be called, so we may never read from this channel; so, make this buffered to allow imageLoadGoroutine to write status and terminate even if we never read it. statusChannel := make(chan error, 1) - ctx, goroutineCancel := context.WithCancel(context.Background()) - go imageLoadGoroutine(ctx, c, reader, statusChannel) + goroutineContext, goroutineCancel := context.WithCancel(context.Background()) + go imageLoadGoroutine(goroutineContext, c, reader, statusChannel) return &daemonImageDestination{ ref: ref, diff --git a/docker/daemon/daemon_src.go b/docker/daemon/daemon_src.go index 644dbeecde..c08640f2c8 100644 --- a/docker/daemon/daemon_src.go +++ b/docker/daemon/daemon_src.go @@ -7,7 +7,6 @@ import ( "github.com/containers/image/docker/tarfile" "github.com/containers/image/types" - "github.com/docker/docker/client" "github.com/pkg/errors" "golang.org/x/net/context" ) @@ -35,7 +34,7 @@ type layerInfo struct { // is the config, and that the following len(RootFS) files are the layers, but that feels // way too brittle.) func newImageSource(ctx *types.SystemContext, ref daemonReference) (types.ImageSource, error) { - c, err := client.NewClient(client.DefaultDockerHost, "1.22", nil, nil) // FIXME: overridable host + c, err := newDockerClient(ctx) if err != nil { return nil, errors.Wrap(err, "Error initializing docker engine client") } diff --git a/docker/daemon/testdata/certs/ca.pem b/docker/daemon/testdata/certs/ca.pem new file mode 100644 index 0000000000..3fc3820b64 --- /dev/null +++ b/docker/daemon/testdata/certs/ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIICzjCCAbagAwIBAgIRAIGgYBNZse0EqRVzxe7aQGIwDQYJKoZIhvcNAQELBQAw +EDEOMAwGA1UEChMFaGFyZHkwHhcNMTcxMDA0MDgzNDAwWhcNMjAwOTE4MDgzNDAw +WjAQMQ4wDAYDVQQKEwVoYXJkeTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMlrdtoXWlZMPFwgeKZHrGxjVe4KXkQy5MFBUfO48htyIe2OlZAd3HGyap41 +7L4YciFhw0bp7wHnYtSTiCHQrnA4SLzNuaU2NM5nJw+E4c5kNrkvhLJqpTNCaYCy +Xbh3H8REW+5UJIgnyeKLx//kvlDm6p4O55+OLlGgzxNaTIgldKLPmx543VVt6VDT +qgFlaYsRz8hZ12+qAqu5am/Wpfal2+Df7Pmmn5M90UBTUwY8CLc/ZiWbv6hihDWV +I28JoM0onEqAx7phRd0SwwK4mYfEe/u614r3bZaI36e9ojU9/St4nbMoMeyZP96t +DOdX9A1SMbsqLOYKXBKM+jXPEaECAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKsMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBALah7CjpwbEY6yjA2KDv +VaAHEgz4Xd8USW/L2292EiQLmFdIaEJiiWRjtKFiF427TXfAPXvxHA2q9OElDW4d +G6XAcBJg5mcBh8WRTHwfLQ8llfj7dH1/sfazSUZeat6lTIyhQfkF99LAJTqlfYAF +aNqIQio7FAjGyJqIPYLa1FKmfLdZr9azb9IjTZLhBGBWdLF0+JOn+JBsl7g9BvUp +ArCI0Wib/vsr368xkzWzKjij1exZdfw0TmsieNYvViFoFJGNCB5XLPo0bHrmMVVe +25EGam+xPkG/JQP5Eb3iikSEn8y5SIeJ0nS0EQE6uXPv+lQj1LmVv8OYzjXqpoJT +n6g= +-----END CERTIFICATE----- diff --git a/docker/daemon/testdata/certs/cert.pem b/docker/daemon/testdata/certs/cert.pem new file mode 100644 index 0000000000..f4d8edeb03 --- /dev/null +++ b/docker/daemon/testdata/certs/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6zCCAdOgAwIBAgIQEh1UsPL20u9KnyOByuhYWDANBgkqhkiG9w0BAQsFADAQ +MQ4wDAYDVQQKEwVoYXJkeTAeFw0xNzEwMDQwODM0MDBaFw0yMDA5MTgwODM0MDBa +MBwxGjAYBgNVBAoMEWhhcmR5Ljxib290c3RyYXA+MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAyJm29vB/urzreEwF012iAAWW3fgE1VEeNLTP/sZTYV3z +UNGKao5x7dUIiah8rptkK3+FN4TID8Z2c1DpKzMTisdpRF3UoRWmjm1UTbxEENhk +EptkFwGFM6BcZSyiLlyCBVM+wGsqzHAASe833S/yiu8miNc2S+jd0FIluKWe0yzG +u2oaJfA28dBfqWyn9hh6msqBVYK6sDle9t0ditNubCyD+vrnoK8825LOIPV6QafL +kVyW0/mj4GJutPOVop37HyQMcuQnDWBA+934l3tpeaJ93d3u8XjU7dXuOobKMohw ++33/pTALu9P0WtDbEeo/xcEICgimqpir92KMSXxUbwIDAQABozUwMzAOBgNVHQ8B +Af8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADANBgkq +hkiG9w0BAQsFAAOCAQEAnYffv9ipGQVW/t3sFxKu9LXQ7ZkhUSgoxPIA51goaYop +YM9QR3ZBf2tMJwjKXuOLEkxxweNjP3dMKh2gykFory+jv6OQYIiLf9M82ty8rOPi +mWLMDAIWWagkj5Yy6b+/aLkpXQ+lEsxLyi6po+D+I+AwRUYvfSc74a7XxkJk77JF +/0SVgNdDtL08zVNOGDgepP/95e1pKMKgsOiCDnFCOAY+l6HcvizwBH+EI+XtdLVb +qBmOAYiwYObBaRuyhVbbDKqKRGFUNkmmDv6vCQoTL1C9wrBnAiJe2khbLm1ix9Re +3MW15CLuipneSgRAWXSdMbDIv9+KQE8fo2TWqikrCw== +-----END CERTIFICATE----- diff --git a/docker/daemon/testdata/certs/key.pem b/docker/daemon/testdata/certs/key.pem new file mode 100644 index 0000000000..ede7d2e1ea --- /dev/null +++ b/docker/daemon/testdata/certs/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAyJm29vB/urzreEwF012iAAWW3fgE1VEeNLTP/sZTYV3zUNGK +ao5x7dUIiah8rptkK3+FN4TID8Z2c1DpKzMTisdpRF3UoRWmjm1UTbxEENhkEptk +FwGFM6BcZSyiLlyCBVM+wGsqzHAASe833S/yiu8miNc2S+jd0FIluKWe0yzGu2oa +JfA28dBfqWyn9hh6msqBVYK6sDle9t0ditNubCyD+vrnoK8825LOIPV6QafLkVyW +0/mj4GJutPOVop37HyQMcuQnDWBA+934l3tpeaJ93d3u8XjU7dXuOobKMohw+33/ +pTALu9P0WtDbEeo/xcEICgimqpir92KMSXxUbwIDAQABAoIBAQCyuKjXR5w1Ll4I +FotWLmTH6jLo3jDIMPZddP6e+emNpRvD1HyixPhiMdvicXdsRUuwqXNx7F4mF+au +hNbIwz/U9CcoXwSy48w5ttRWUba+31wBa+p3yMX5IhVPmr1/2rGItwsAejpuXBcV +yAiYi0BnYfyODFf2t6jwElBDO2POtdEoYVYwgtMTMy5pmDA2QA3mKkjCcJviectZ +9yFb8DFiwIYkryErWrGWaKls/oYV2O0A0mCaIqgw3HfhIl6F1pk+9oYnmsq6IzF5 +wSIg2evd4GMm/L2sqlVFqb4Kj54fbyfdOFK0bQso6VQZvB5tZ6NLHfv2f3BBFHVu +jO+On/ixAoGBAOJkPHavnAb/lLDnMJjjXYNUQqyxxSlwOwNifG4evf/KAezIIalJ +kC7jZoFsUkARVbRKQag0T2Xvxw/dDqmNastR1NxsBkhOWjYiQbALYP3u2f06Nhf8 +YlX6hyEje/3bb838//sH5jnaN8GcZnDBrAoPzW+V87pQoCyVrjs2t8qXAoGBAOLV ++PviAUWFjUO//dYk9H9IWotr6rdkzmpLbrj+NoLNSGeoZbByPmT5BuNswXvNyk+9 +smOQ8yqBiMpjxKwR4WQnS6Ydh6HTT33IWLLVazDFMf7ACmXWoScFhCAW6qGfdrYQ +hkCSbwgun8jbL2D477jJl6ZyQG48lVnnZDjkFbfpAoGAUOqCsekSW23+Nzxqojqh +sc7sBc2EKstyTENnNfTG9CW/imH9pgQlBJ1Chf+xZjTL7SSdUwFfX4/UFldsZi2l +fgZBjocNt8pJdA/KaqGmiRxVzayAqRIME673nWCRcKp9y6Ih3Bd2sjbMtuavtp2C +YBZF1xxBgNZQaZ8WJxPnnQECgYEAzLgGJPWc5iyZCJsesQTbMICRTyEPTYKKFD6N +6CFt+vDgNsUxOWRx0Vk6kUhW+rAItZzjgZ6RBzyuwtH17sGYZHZefMZL4Y2/QSru +ej/IpNRjwaF6AN0KxhfhXcCw8zrivX/+WgqOcJj7lh/TC7a/S0uNNSgJ5DODKwd9 +WSboPvkCgYEAzqdWfetko7hEI4076pufJrHPnnCJSHkkQ1QnfVl71mq7UmKXLDxD +L5oWtU53+dswzvxGrzkOWsRJC5nN30BYJuYlwKzo3+MCKlUzJSuIMVTbTPlwKudh +AF19s4GFZVo29FlgIQhA5dfIkZgFXAlVxYcGTLUixEmPwrc6yguULPs= +-----END RSA PRIVATE KEY----- diff --git a/types/types.go b/types/types.go index e955c33806..93e12e638a 100644 --- a/types/types.go +++ b/types/types.go @@ -283,7 +283,7 @@ type DockerAuthConfig struct { Password string } -// SystemContext allows parametrizing access to implicitly-accessed resources, +// SystemContext allows parameterizing access to implicitly-accessed resources, // like configuration files in /etc and users' login state in their home directory. // Various components can share the same field only if their semantics is exactly // the same; if in doubt, add a new field. @@ -320,8 +320,9 @@ type SystemContext struct { DockerCertPath string // If not "", overrides the system’s default path for a directory containing host[:port] subdirectories with the same structure as DockerCertPath above. // Ignored if DockerCertPath is non-empty. - DockerPerHostCertDirPath string - DockerInsecureSkipTLSVerify bool // Allow contacting docker registries over HTTP, or HTTPS with failed TLS verification. Note that this does not affect other TLS connections. + DockerPerHostCertDirPath string + // Allow contacting docker registries over HTTP, or HTTPS with failed TLS verification. Note that this does not affect other TLS connections. + DockerInsecureSkipTLSVerify bool // if nil, the library tries to parse ~/.docker/config.json to retrieve credentials DockerAuthConfig *DockerAuthConfig // if not "", an User-Agent header is added to each request when contacting a registry. @@ -332,6 +333,18 @@ type SystemContext struct { DockerDisableV1Ping bool // Directory to use for OSTree temporary files OSTreeTmpDirPath string + + // === docker/daemon.Transport overrides === + // A directory containing a CA certificate (ending with ".crt"), + // a client certificate (ending with ".cert") and a client ceritificate key + // (ending with ".key") used when talking to a Docker daemon. + DockerDaemonCertPath string + // The hostname or IP to the Docker daemon. If not set (aka ""), client.DefaultDockerHost is assumed. + DockerDaemonHost string + // API version of the Docker daemon to talk to. + DockerDaemonAPIVersion string + // Used to skip TLS verification, off by default. + DockerDaemonInsecureSkipTLSVerify bool } // ProgressProperties is used to pass information from the copy code to a monitor which