2016-11-22 19:32:10 +00:00
package daemon
import (
"io"
"github.com/Sirupsen/logrus"
"github.com/containers/image/docker/reference"
2017-04-27 18:00:07 +00:00
"github.com/containers/image/docker/tarfile"
2016-11-22 19:32:10 +00:00
"github.com/containers/image/types"
2017-02-01 00:45:59 +00:00
"github.com/docker/docker/client"
2016-10-17 13:53:40 +00:00
"github.com/pkg/errors"
2016-11-22 19:32:10 +00:00
"golang.org/x/net/context"
)
type daemonImageDestination struct {
2017-04-27 18:00:07 +00:00
ref daemonReference
* tarfile . Destination // Implements most of types.ImageDestination
2016-11-22 19:32:10 +00:00
// For talking to imageLoadGoroutine
goroutineCancel context . CancelFunc
statusChannel <- chan error
writer * io . PipeWriter
// Other state
2017-04-27 18:00:07 +00:00
committed bool // writer has been closed
2016-11-22 19:32:10 +00:00
}
// newImageDestination returns a types.ImageDestination for the specified image reference.
func newImageDestination ( systemCtx * types . SystemContext , ref daemonReference ) ( types . ImageDestination , error ) {
if ref . ref == nil {
2016-10-17 13:53:40 +00:00
return nil , errors . Errorf ( "Invalid destination docker-daemon:%s: a destination must be a name:tag" , ref . StringWithinTransport ( ) )
2016-11-22 19:32:10 +00:00
}
namedTaggedRef , ok := ref . ref . ( reference . NamedTagged )
if ! ok {
2016-10-17 13:53:40 +00:00
return nil , errors . Errorf ( "Invalid destination docker-daemon:%s: a destination must be a name:tag" , ref . StringWithinTransport ( ) )
2016-11-22 19:32:10 +00:00
}
c , err := client . NewClient ( client . DefaultDockerHost , "1.22" , nil , nil ) // FIXME: overridable host
if err != nil {
2016-10-17 13:53:40 +00:00
return nil , errors . Wrap ( err , "Error initializing docker engine client" )
2016-11-22 19:32:10 +00:00
}
reader , writer := io . Pipe ( )
// 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 )
return & daemonImageDestination {
ref : ref ,
2017-04-27 18:00:07 +00:00
Destination : tarfile . NewDestination ( writer , namedTaggedRef ) ,
2016-11-22 19:32:10 +00:00
goroutineCancel : goroutineCancel ,
statusChannel : statusChannel ,
writer : writer ,
committed : false ,
} , nil
}
// imageLoadGoroutine accepts tar stream on reader, sends it to c, and reports error or success by writing to statusChannel
func imageLoadGoroutine ( ctx context . Context , c * client . Client , reader * io . PipeReader , statusChannel chan <- error ) {
err := errors . New ( "Internal error: unexpected panic in imageLoadGoroutine" )
defer func ( ) {
logrus . Debugf ( "docker-daemon: sending done, status %v" , err )
statusChannel <- err
} ( )
defer func ( ) {
if err == nil {
reader . Close ( )
} else {
reader . CloseWithError ( err )
}
} ( )
resp , err := c . ImageLoad ( ctx , reader , true )
if err != nil {
2016-10-17 13:53:40 +00:00
err = errors . Wrap ( err , "Error saving image to docker engine" )
2016-11-22 19:32:10 +00:00
return
}
defer resp . Body . Close ( )
}
// Close removes resources associated with an initialized ImageDestination, if any.
2017-03-13 16:33:17 +00:00
func ( d * daemonImageDestination ) Close ( ) error {
2016-11-22 19:32:10 +00:00
if ! d . committed {
logrus . Debugf ( "docker-daemon: Closing tar stream to abort loading" )
// In principle, goroutineCancel() should abort the HTTP request and stop the process from continuing.
2017-02-01 00:45:59 +00:00
// In practice, though, various HTTP implementations used by client.Client.ImageLoad() (including
// https://github.com/golang/net/blob/master/context/ctxhttp/ctxhttp_pre17.go and the
// net/http version with native Context support in Go 1.7) do not always actually immediately cancel
// the operation: they may process the HTTP request, or a part of it, to completion in a goroutine, and
// return early if the context is canceled without terminating the goroutine at all.
// So we need this CloseWithError to terminate sending the HTTP request Body
2016-11-22 19:32:10 +00:00
// immediately, and hopefully, through terminating the sending which uses "Transfer-Encoding: chunked"" without sending
// the terminating zero-length chunk, prevent the docker daemon from processing the tar stream at all.
// Whether that works or not, closing the PipeWriter seems desirable in any case.
d . writer . CloseWithError ( errors . New ( "Aborting upload, daemonImageDestination closed without a previous .Commit()" ) )
}
d . goroutineCancel ( )
2017-03-13 16:33:17 +00:00
return nil
2016-11-22 19:32:10 +00:00
}
func ( d * daemonImageDestination ) Reference ( ) types . ImageReference {
return d . ref
}
// Commit marks the process of storing the image as successful and asks for the image to be persisted.
// WARNING: This does not have any transactional semantics:
// - Uploaded data MAY be visible to others before Commit() is called
// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed)
func ( d * daemonImageDestination ) Commit ( ) error {
logrus . Debugf ( "docker-daemon: Closing tar stream" )
2017-04-27 18:00:07 +00:00
if err := d . Destination . Commit ( ) ; err != nil {
2016-11-22 19:32:10 +00:00
return err
}
if err := d . writer . Close ( ) ; err != nil {
return err
}
d . committed = true // We may still fail, but we are done sending to imageLoadGoroutine.
logrus . Debugf ( "docker-daemon: Waiting for status" )
err := <- d . statusChannel
return err
}