2014-03-28 19:21:55 -04:00
package client
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
gosignal "os/signal"
"regexp"
goruntime "runtime"
"strconv"
"strings"
"syscall"
"github.com/dotcloud/docker/api"
"github.com/dotcloud/docker/dockerversion"
"github.com/dotcloud/docker/engine"
"github.com/dotcloud/docker/pkg/term"
"github.com/dotcloud/docker/registry"
"github.com/dotcloud/docker/utils"
)
var (
ErrConnectionRefused = errors . New ( "Cannot connect to the Docker daemon. Is 'docker -d' running on this host?" )
)
func ( cli * DockerCli ) dial ( ) ( net . Conn , error ) {
if cli . tlsConfig != nil && cli . proto != "unix" {
return tls . Dial ( cli . proto , cli . addr , cli . tlsConfig )
}
return net . Dial ( cli . proto , cli . addr )
}
func ( cli * DockerCli ) call ( method , path string , data interface { } , passAuthInfo bool ) ( io . ReadCloser , int , error ) {
params := bytes . NewBuffer ( nil )
if data != nil {
if env , ok := data . ( engine . Env ) ; ok {
if err := env . Encode ( params ) ; err != nil {
return nil , - 1 , err
}
} else {
buf , err := json . Marshal ( data )
if err != nil {
return nil , - 1 , err
}
if _ , err := params . Write ( buf ) ; err != nil {
return nil , - 1 , err
}
}
}
// fixme: refactor client to support redirect
re := regexp . MustCompile ( "/+" )
path = re . ReplaceAllString ( path , "/" )
req , err := http . NewRequest ( method , fmt . Sprintf ( "/v%s%s" , api . APIVERSION , path ) , params )
if err != nil {
return nil , - 1 , err
}
if passAuthInfo {
cli . LoadConfigFile ( )
// Resolve the Auth config relevant for this server
authConfig := cli . configFile . ResolveAuthConfig ( registry . IndexServerAddress ( ) )
getHeaders := func ( authConfig registry . AuthConfig ) ( map [ string ] [ ] string , error ) {
buf , err := json . Marshal ( authConfig )
if err != nil {
return nil , err
}
registryAuthHeader := [ ] string {
base64 . URLEncoding . EncodeToString ( buf ) ,
}
return map [ string ] [ ] string { "X-Registry-Auth" : registryAuthHeader } , nil
}
if headers , err := getHeaders ( authConfig ) ; err == nil && headers != nil {
for k , v := range headers {
req . Header [ k ] = v
}
}
}
req . Header . Set ( "User-Agent" , "Docker-Client/" + dockerversion . VERSION )
req . Host = cli . addr
if data != nil {
req . Header . Set ( "Content-Type" , "application/json" )
} else if method == "POST" {
req . Header . Set ( "Content-Type" , "plain/text" )
}
dial , err := cli . dial ( )
if err != nil {
if strings . Contains ( err . Error ( ) , "connection refused" ) {
return nil , - 1 , ErrConnectionRefused
}
return nil , - 1 , err
}
clientconn := httputil . NewClientConn ( dial , nil )
resp , err := clientconn . Do ( req )
if err != nil {
clientconn . Close ( )
if strings . Contains ( err . Error ( ) , "connection refused" ) {
return nil , - 1 , ErrConnectionRefused
}
return nil , - 1 , err
}
if resp . StatusCode < 200 || resp . StatusCode >= 400 {
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return nil , - 1 , err
}
if len ( body ) == 0 {
return nil , resp . StatusCode , fmt . Errorf ( "Error: request returned %s for API route and version %s, check if the server supports the requested API version" , http . StatusText ( resp . StatusCode ) , req . URL )
}
return nil , resp . StatusCode , fmt . Errorf ( "Error: %s" , bytes . TrimSpace ( body ) )
}
wrapper := utils . NewReadCloserWrapper ( resp . Body , func ( ) error {
if resp != nil && resp . Body != nil {
resp . Body . Close ( )
}
return clientconn . Close ( )
} )
return wrapper , resp . StatusCode , nil
}
func ( cli * DockerCli ) stream ( method , path string , in io . Reader , out io . Writer , headers map [ string ] [ ] string ) error {
if ( method == "POST" || method == "PUT" ) && in == nil {
in = bytes . NewReader ( [ ] byte { } )
}
// fixme: refactor client to support redirect
re := regexp . MustCompile ( "/+" )
path = re . ReplaceAllString ( path , "/" )
req , err := http . NewRequest ( method , fmt . Sprintf ( "/v%s%s" , api . APIVERSION , path ) , in )
if err != nil {
return err
}
req . Header . Set ( "User-Agent" , "Docker-Client/" + dockerversion . VERSION )
req . Host = cli . addr
if method == "POST" {
req . Header . Set ( "Content-Type" , "plain/text" )
}
if headers != nil {
for k , v := range headers {
req . Header [ k ] = v
}
}
dial , err := cli . dial ( )
if err != nil {
if strings . Contains ( err . Error ( ) , "connection refused" ) {
return fmt . Errorf ( "Cannot connect to the Docker daemon. Is 'docker -d' running on this host?" )
}
return err
}
clientconn := httputil . NewClientConn ( dial , nil )
resp , err := clientconn . Do ( req )
defer clientconn . Close ( )
if err != nil {
if strings . Contains ( err . Error ( ) , "connection refused" ) {
return fmt . Errorf ( "Cannot connect to the Docker daemon. Is 'docker -d' running on this host?" )
}
return err
}
defer resp . Body . Close ( )
if resp . StatusCode < 200 || resp . StatusCode >= 400 {
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return err
}
if len ( body ) == 0 {
return fmt . Errorf ( "Error :%s" , http . StatusText ( resp . StatusCode ) )
}
return fmt . Errorf ( "Error: %s" , bytes . TrimSpace ( body ) )
}
if api . MatchesContentType ( resp . Header . Get ( "Content-Type" ) , "application/json" ) {
return utils . DisplayJSONMessagesStream ( resp . Body , out , cli . terminalFd , cli . isTerminal )
}
if _ , err := io . Copy ( out , resp . Body ) ; err != nil {
return err
}
return nil
}
func ( cli * DockerCli ) hijack ( method , path string , setRawTerminal bool , in io . ReadCloser , stdout , stderr io . Writer , started chan io . Closer ) error {
defer func ( ) {
if started != nil {
close ( started )
}
} ( )
// fixme: refactor client to support redirect
re := regexp . MustCompile ( "/+" )
path = re . ReplaceAllString ( path , "/" )
req , err := http . NewRequest ( method , fmt . Sprintf ( "/v%s%s" , api . APIVERSION , path ) , nil )
if err != nil {
return err
}
req . Header . Set ( "User-Agent" , "Docker-Client/" + dockerversion . VERSION )
req . Header . Set ( "Content-Type" , "plain/text" )
req . Host = cli . addr
dial , err := cli . dial ( )
if err != nil {
if strings . Contains ( err . Error ( ) , "connection refused" ) {
return fmt . Errorf ( "Cannot connect to the Docker daemon. Is 'docker -d' running on this host?" )
}
return err
}
clientconn := httputil . NewClientConn ( dial , nil )
defer clientconn . Close ( )
// Server hijacks the connection, error 'connection closed' expected
clientconn . Do ( req )
rwc , br := clientconn . Hijack ( )
defer rwc . Close ( )
if started != nil {
started <- rwc
}
var receiveStdout chan error
var oldState * term . State
if in != nil && setRawTerminal && cli . isTerminal && os . Getenv ( "NORAW" ) == "" {
oldState , err = term . SetRawTerminal ( cli . terminalFd )
if err != nil {
return err
}
defer term . RestoreTerminal ( cli . terminalFd , oldState )
}
if stdout != nil || stderr != nil {
receiveStdout = utils . Go ( func ( ) ( err error ) {
defer func ( ) {
if in != nil {
if setRawTerminal && cli . isTerminal {
term . RestoreTerminal ( cli . terminalFd , oldState )
}
// For some reason this Close call blocks on darwin..
// As the client exists right after, simply discard the close
// until we find a better solution.
if goruntime . GOOS != "darwin" {
in . Close ( )
}
}
} ( )
// When TTY is ON, use regular copy
if setRawTerminal {
_ , err = io . Copy ( stdout , br )
} else {
_ , err = utils . StdCopy ( stdout , stderr , br )
}
utils . Debugf ( "[hijack] End of stdout" )
return err
} )
}
sendStdin := utils . Go ( func ( ) error {
if in != nil {
io . Copy ( rwc , in )
utils . Debugf ( "[hijack] End of stdin" )
}
if tcpc , ok := rwc . ( * net . TCPConn ) ; ok {
if err := tcpc . CloseWrite ( ) ; err != nil {
2014-03-31 14:08:46 -04:00
utils . Debugf ( "Couldn't send EOF: %s\n" , err )
2014-03-28 19:21:55 -04:00
}
} else if unixc , ok := rwc . ( * net . UnixConn ) ; ok {
if err := unixc . CloseWrite ( ) ; err != nil {
2014-03-31 14:08:46 -04:00
utils . Debugf ( "Couldn't send EOF: %s\n" , err )
2014-03-28 19:21:55 -04:00
}
}
// Discard errors due to pipe interruption
return nil
} )
if stdout != nil || stderr != nil {
if err := <- receiveStdout ; err != nil {
2014-03-31 14:08:46 -04:00
utils . Debugf ( "Error receiveStdout: %s" , err )
2014-03-28 19:21:55 -04:00
return err
}
}
if ! cli . isTerminal {
if err := <- sendStdin ; err != nil {
2014-03-31 14:08:46 -04:00
utils . Debugf ( "Error sendStdin: %s" , err )
2014-03-28 19:21:55 -04:00
return err
}
}
return nil
}
func ( cli * DockerCli ) resizeTty ( id string ) {
height , width := cli . getTtySize ( )
if height == 0 && width == 0 {
return
}
v := url . Values { }
v . Set ( "h" , strconv . Itoa ( height ) )
v . Set ( "w" , strconv . Itoa ( width ) )
if _ , _ , err := readBody ( cli . call ( "POST" , "/containers/" + id + "/resize?" + v . Encode ( ) , nil , false ) ) ; err != nil {
2014-03-31 14:08:46 -04:00
utils . Debugf ( "Error resize: %s" , err )
2014-03-28 19:21:55 -04:00
}
}
func waitForExit ( cli * DockerCli , containerId string ) ( int , error ) {
stream , _ , err := cli . call ( "POST" , "/containers/" + containerId + "/wait" , nil , false )
if err != nil {
return - 1 , err
}
var out engine . Env
if err := out . Decode ( stream ) ; err != nil {
return - 1 , err
}
return out . GetInt ( "StatusCode" ) , nil
}
// getExitCode perform an inspect on the container. It returns
// the running state and the exit code.
func getExitCode ( cli * DockerCli , containerId string ) ( bool , int , error ) {
body , _ , err := readBody ( cli . call ( "GET" , "/containers/" + containerId + "/json" , nil , false ) )
if err != nil {
// If we can't connect, then the daemon probably died.
if err != ErrConnectionRefused {
return false , - 1 , err
}
return false , - 1 , nil
}
c := & api . Container { }
if err := json . Unmarshal ( body , c ) ; err != nil {
return false , - 1 , err
}
return c . State . Running , c . State . ExitCode , nil
}
func ( cli * DockerCli ) monitorTtySize ( id string ) error {
cli . resizeTty ( id )
sigchan := make ( chan os . Signal , 1 )
gosignal . Notify ( sigchan , syscall . SIGWINCH )
go func ( ) {
for _ = range sigchan {
cli . resizeTty ( id )
}
} ( )
return nil
}
func ( cli * DockerCli ) getTtySize ( ) ( int , int ) {
if ! cli . isTerminal {
return 0 , 0
}
ws , err := term . GetWinsize ( cli . terminalFd )
if err != nil {
2014-03-31 14:08:46 -04:00
utils . Debugf ( "Error getting size: %s" , err )
2014-03-28 19:21:55 -04:00
if ws == nil {
return 0 , 0
}
}
return int ( ws . Height ) , int ( ws . Width )
}
func readBody ( stream io . ReadCloser , statusCode int , err error ) ( [ ] byte , int , error ) {
if stream != nil {
defer stream . Close ( )
}
if err != nil {
return nil , statusCode , err
}
body , err := ioutil . ReadAll ( stream )
if err != nil {
return nil , - 1 , err
}
return body , statusCode , nil
}