2014-08-07 14:43:06 +00:00
package registry
import (
"bytes"
"crypto/sha256"
_ "crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"net/url"
"strconv"
"strings"
"time"
"github.com/docker/docker/pkg/httputils"
2014-08-13 22:13:21 +00:00
"github.com/docker/docker/pkg/log"
2014-08-07 14:43:06 +00:00
"github.com/docker/docker/pkg/tarsum"
"github.com/docker/docker/utils"
)
type Session struct {
authConfig * AuthConfig
reqFactory * utils . HTTPRequestFactory
indexEndpoint string
jar * cookiejar . Jar
timeout TimeoutType
}
func NewSession ( authConfig * AuthConfig , factory * utils . HTTPRequestFactory , indexEndpoint string , timeout bool ) ( r * Session , err error ) {
r = & Session {
authConfig : authConfig ,
indexEndpoint : indexEndpoint ,
}
if timeout {
r . timeout = ReceiveTimeout
}
r . jar , err = cookiejar . New ( nil )
if err != nil {
return nil , err
}
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
// alongside our requests.
if indexEndpoint != IndexServerAddress ( ) && strings . HasPrefix ( indexEndpoint , "https://" ) {
info , err := pingRegistryEndpoint ( indexEndpoint )
if err != nil {
return nil , err
}
if info . Standalone {
2014-08-13 22:13:21 +00:00
log . Debugf ( "Endpoint %s is eligible for private registry registry. Enabling decorator." , indexEndpoint )
2014-08-07 14:43:06 +00:00
dec := utils . NewHTTPAuthDecorator ( authConfig . Username , authConfig . Password )
factory . AddDecorator ( dec )
}
}
r . reqFactory = factory
return r , nil
}
func ( r * Session ) doRequest ( req * http . Request ) ( * http . Response , * http . Client , error ) {
return doRequest ( req , r . jar , r . timeout )
}
// Retrieve the history of a given image from the Registry.
// Return a list of the parent's json (requested image included)
func ( r * Session ) GetRemoteHistory ( imgID , registry string , token [ ] string ) ( [ ] string , error ) {
req , err := r . reqFactory . NewRequest ( "GET" , registry + "images/" + imgID + "/ancestry" , nil )
if err != nil {
return nil , err
}
setTokenAuth ( req , token )
res , _ , err := r . doRequest ( req )
if err != nil {
return nil , err
}
defer res . Body . Close ( )
if res . StatusCode != 200 {
if res . StatusCode == 401 {
return nil , errLoginRequired
}
return nil , utils . NewHTTPRequestError ( fmt . Sprintf ( "Server error: %d trying to fetch remote history for %s" , res . StatusCode , imgID ) , res )
}
jsonString , err := ioutil . ReadAll ( res . Body )
if err != nil {
return nil , fmt . Errorf ( "Error while reading the http response: %s" , err )
}
2014-08-13 22:13:21 +00:00
log . Debugf ( "Ancestry: %s" , jsonString )
2014-08-07 14:43:06 +00:00
history := new ( [ ] string )
if err := json . Unmarshal ( jsonString , history ) ; err != nil {
return nil , err
}
return * history , nil
}
// Check if an image exists in the Registry
// TODO: This method should return the errors instead of masking them and returning false
func ( r * Session ) LookupRemoteImage ( imgID , registry string , token [ ] string ) bool {
req , err := r . reqFactory . NewRequest ( "GET" , registry + "images/" + imgID + "/json" , nil )
if err != nil {
2014-08-13 22:13:21 +00:00
log . Errorf ( "Error in LookupRemoteImage %s" , err )
2014-08-07 14:43:06 +00:00
return false
}
setTokenAuth ( req , token )
res , _ , err := r . doRequest ( req )
if err != nil {
2014-08-13 22:13:21 +00:00
log . Errorf ( "Error in LookupRemoteImage %s" , err )
2014-08-07 14:43:06 +00:00
return false
}
res . Body . Close ( )
return res . StatusCode == 200
}
// Retrieve an image from the Registry.
func ( r * Session ) GetRemoteImageJSON ( imgID , registry string , token [ ] string ) ( [ ] byte , int , error ) {
// Get the JSON
req , err := r . reqFactory . NewRequest ( "GET" , registry + "images/" + imgID + "/json" , nil )
if err != nil {
return nil , - 1 , fmt . Errorf ( "Failed to download json: %s" , err )
}
setTokenAuth ( req , token )
res , _ , err := r . doRequest ( req )
if err != nil {
return nil , - 1 , fmt . Errorf ( "Failed to download json: %s" , err )
}
defer res . Body . Close ( )
if res . StatusCode != 200 {
return nil , - 1 , utils . NewHTTPRequestError ( fmt . Sprintf ( "HTTP code %d" , res . StatusCode ) , res )
}
// if the size header is not present, then set it to '-1'
imageSize := - 1
if hdr := res . Header . Get ( "X-Docker-Size" ) ; hdr != "" {
imageSize , err = strconv . Atoi ( hdr )
if err != nil {
return nil , - 1 , err
}
}
jsonString , err := ioutil . ReadAll ( res . Body )
if err != nil {
return nil , - 1 , fmt . Errorf ( "Failed to parse downloaded json: %s (%s)" , err , jsonString )
}
return jsonString , imageSize , nil
}
func ( r * Session ) GetRemoteImageLayer ( imgID , registry string , token [ ] string , imgSize int64 ) ( io . ReadCloser , error ) {
var (
retries = 5
client * http . Client
res * http . Response
imageURL = fmt . Sprintf ( "%simages/%s/layer" , registry , imgID )
)
req , err := r . reqFactory . NewRequest ( "GET" , imageURL , nil )
if err != nil {
return nil , fmt . Errorf ( "Error while getting from the server: %s\n" , err )
}
setTokenAuth ( req , token )
for i := 1 ; i <= retries ; i ++ {
res , client , err = r . doRequest ( req )
if err != nil {
res . Body . Close ( )
if i == retries {
return nil , fmt . Errorf ( "Server error: Status %d while fetching image layer (%s)" ,
res . StatusCode , imgID )
}
time . Sleep ( time . Duration ( i ) * 5 * time . Second )
continue
}
break
}
if res . StatusCode != 200 {
res . Body . Close ( )
return nil , fmt . Errorf ( "Server error: Status %d while fetching image layer (%s)" ,
res . StatusCode , imgID )
}
if res . Header . Get ( "Accept-Ranges" ) == "bytes" && imgSize > 0 {
2014-08-13 22:13:21 +00:00
log . Debugf ( "server supports resume" )
2014-08-07 14:43:06 +00:00
return httputils . ResumableRequestReaderWithInitialResponse ( client , req , 5 , imgSize , res ) , nil
}
2014-08-13 22:13:21 +00:00
log . Debugf ( "server doesn't support resume" )
2014-08-07 14:43:06 +00:00
return res . Body , nil
}
func ( r * Session ) GetRemoteTags ( registries [ ] string , repository string , token [ ] string ) ( map [ string ] string , error ) {
if strings . Count ( repository , "/" ) == 0 {
// This will be removed once the Registry supports auto-resolution on
// the "library" namespace
repository = "library/" + repository
}
for _ , host := range registries {
endpoint := fmt . Sprintf ( "%srepositories/%s/tags" , host , repository )
req , err := r . reqFactory . NewRequest ( "GET" , endpoint , nil )
if err != nil {
return nil , err
}
setTokenAuth ( req , token )
res , _ , err := r . doRequest ( req )
if err != nil {
return nil , err
}
2014-08-13 22:13:21 +00:00
log . Debugf ( "Got status code %d from %s" , res . StatusCode , endpoint )
2014-08-07 14:43:06 +00:00
defer res . Body . Close ( )
if res . StatusCode != 200 && res . StatusCode != 404 {
continue
} else if res . StatusCode == 404 {
return nil , fmt . Errorf ( "Repository not found" )
}
result := make ( map [ string ] string )
rawJSON , err := ioutil . ReadAll ( res . Body )
if err != nil {
return nil , err
}
if err := json . Unmarshal ( rawJSON , & result ) ; err != nil {
return nil , err
}
return result , nil
}
return nil , fmt . Errorf ( "Could not reach any registry endpoint" )
}
func buildEndpointsList ( headers [ ] string , indexEp string ) ( [ ] string , error ) {
var endpoints [ ] string
parsedUrl , err := url . Parse ( indexEp )
if err != nil {
return nil , err
}
var urlScheme = parsedUrl . Scheme
// The Registry's URL scheme has to match the Index'
for _ , ep := range headers {
epList := strings . Split ( ep , "," )
for _ , epListElement := range epList {
endpoints = append (
endpoints ,
fmt . Sprintf ( "%s://%s/v1/" , urlScheme , strings . TrimSpace ( epListElement ) ) )
}
}
return endpoints , nil
}
func ( r * Session ) GetRepositoryData ( remote string ) ( * RepositoryData , error ) {
indexEp := r . indexEndpoint
repositoryTarget := fmt . Sprintf ( "%srepositories/%s/images" , indexEp , remote )
2014-08-13 22:13:21 +00:00
log . Debugf ( "[registry] Calling GET %s" , repositoryTarget )
2014-08-07 14:43:06 +00:00
req , err := r . reqFactory . NewRequest ( "GET" , repositoryTarget , nil )
if err != nil {
return nil , err
}
if r . authConfig != nil && len ( r . authConfig . Username ) > 0 {
req . SetBasicAuth ( r . authConfig . Username , r . authConfig . Password )
}
req . Header . Set ( "X-Docker-Token" , "true" )
res , _ , err := r . doRequest ( req )
if err != nil {
return nil , err
}
defer res . Body . Close ( )
if res . StatusCode == 401 {
return nil , errLoginRequired
}
// TODO: Right now we're ignoring checksums in the response body.
// In the future, we need to use them to check image validity.
if res . StatusCode != 200 {
return nil , utils . NewHTTPRequestError ( fmt . Sprintf ( "HTTP code: %d" , res . StatusCode ) , res )
}
var tokens [ ] string
if res . Header . Get ( "X-Docker-Token" ) != "" {
tokens = res . Header [ "X-Docker-Token" ]
}
var endpoints [ ] string
if res . Header . Get ( "X-Docker-Endpoints" ) != "" {
endpoints , err = buildEndpointsList ( res . Header [ "X-Docker-Endpoints" ] , indexEp )
if err != nil {
return nil , err
}
} else {
// Assume the endpoint is on the same host
u , err := url . Parse ( indexEp )
if err != nil {
return nil , err
}
endpoints = append ( endpoints , fmt . Sprintf ( "%s://%s/v1/" , u . Scheme , req . URL . Host ) )
}
checksumsJSON , err := ioutil . ReadAll ( res . Body )
if err != nil {
return nil , err
}
remoteChecksums := [ ] * ImgData { }
if err := json . Unmarshal ( checksumsJSON , & remoteChecksums ) ; err != nil {
return nil , err
}
// Forge a better object from the retrieved data
imgsData := make ( map [ string ] * ImgData )
for _ , elem := range remoteChecksums {
imgsData [ elem . ID ] = elem
}
return & RepositoryData {
ImgList : imgsData ,
Endpoints : endpoints ,
Tokens : tokens ,
} , nil
}
func ( r * Session ) PushImageChecksumRegistry ( imgData * ImgData , registry string , token [ ] string ) error {
2014-08-13 22:13:21 +00:00
log . Debugf ( "[registry] Calling PUT %s" , registry + "images/" + imgData . ID + "/checksum" )
2014-08-07 14:43:06 +00:00
req , err := r . reqFactory . NewRequest ( "PUT" , registry + "images/" + imgData . ID + "/checksum" , nil )
if err != nil {
return err
}
setTokenAuth ( req , token )
req . Header . Set ( "X-Docker-Checksum" , imgData . Checksum )
req . Header . Set ( "X-Docker-Checksum-Payload" , imgData . ChecksumPayload )
res , _ , err := r . doRequest ( req )
if err != nil {
return fmt . Errorf ( "Failed to upload metadata: %s" , err )
}
defer res . Body . Close ( )
if len ( res . Cookies ( ) ) > 0 {
r . jar . SetCookies ( req . URL , res . Cookies ( ) )
}
if res . StatusCode != 200 {
errBody , err := ioutil . ReadAll ( res . Body )
if err != nil {
return fmt . Errorf ( "HTTP code %d while uploading metadata and error when trying to parse response body: %s" , res . StatusCode , err )
}
var jsonBody map [ string ] string
if err := json . Unmarshal ( errBody , & jsonBody ) ; err != nil {
errBody = [ ] byte ( err . Error ( ) )
} else if jsonBody [ "error" ] == "Image already exists" {
return ErrAlreadyExists
}
return fmt . Errorf ( "HTTP code %d while uploading metadata: %s" , res . StatusCode , errBody )
}
return nil
}
// Push a local image to the registry
func ( r * Session ) PushImageJSONRegistry ( imgData * ImgData , jsonRaw [ ] byte , registry string , token [ ] string ) error {
2014-08-13 22:13:21 +00:00
log . Debugf ( "[registry] Calling PUT %s" , registry + "images/" + imgData . ID + "/json" )
2014-08-07 14:43:06 +00:00
req , err := r . reqFactory . NewRequest ( "PUT" , registry + "images/" + imgData . ID + "/json" , bytes . NewReader ( jsonRaw ) )
if err != nil {
return err
}
req . Header . Add ( "Content-type" , "application/json" )
setTokenAuth ( req , token )
res , _ , err := r . doRequest ( req )
if err != nil {
return fmt . Errorf ( "Failed to upload metadata: %s" , err )
}
defer res . Body . Close ( )
if res . StatusCode == 401 && strings . HasPrefix ( registry , "http://" ) {
return utils . NewHTTPRequestError ( "HTTP code 401, Docker will not send auth headers over HTTP." , res )
}
if res . StatusCode != 200 {
errBody , err := ioutil . ReadAll ( res . Body )
if err != nil {
return utils . NewHTTPRequestError ( fmt . Sprintf ( "HTTP code %d while uploading metadata and error when trying to parse response body: %s" , res . StatusCode , err ) , res )
}
var jsonBody map [ string ] string
if err := json . Unmarshal ( errBody , & jsonBody ) ; err != nil {
errBody = [ ] byte ( err . Error ( ) )
} else if jsonBody [ "error" ] == "Image already exists" {
return ErrAlreadyExists
}
return utils . NewHTTPRequestError ( fmt . Sprintf ( "HTTP code %d while uploading metadata: %s" , res . StatusCode , errBody ) , res )
}
return nil
}
func ( r * Session ) PushImageLayerRegistry ( imgID string , layer io . Reader , registry string , token [ ] string , jsonRaw [ ] byte ) ( checksum string , checksumPayload string , err error ) {
2014-08-13 22:13:21 +00:00
log . Debugf ( "[registry] Calling PUT %s" , registry + "images/" + imgID + "/layer" )
2014-08-07 14:43:06 +00:00
tarsumLayer := & tarsum . TarSum { Reader : layer }
h := sha256 . New ( )
h . Write ( jsonRaw )
h . Write ( [ ] byte { '\n' } )
checksumLayer := io . TeeReader ( tarsumLayer , h )
req , err := r . reqFactory . NewRequest ( "PUT" , registry + "images/" + imgID + "/layer" , checksumLayer )
if err != nil {
return "" , "" , err
}
req . Header . Add ( "Content-Type" , "application/octet-stream" )
req . ContentLength = - 1
req . TransferEncoding = [ ] string { "chunked" }
setTokenAuth ( req , token )
res , _ , err := r . doRequest ( req )
if err != nil {
return "" , "" , fmt . Errorf ( "Failed to upload layer: %s" , err )
}
if rc , ok := layer . ( io . Closer ) ; ok {
if err := rc . Close ( ) ; err != nil {
return "" , "" , err
}
}
defer res . Body . Close ( )
if res . StatusCode != 200 {
errBody , err := ioutil . ReadAll ( res . Body )
if err != nil {
return "" , "" , utils . NewHTTPRequestError ( fmt . Sprintf ( "HTTP code %d while uploading metadata and error when trying to parse response body: %s" , res . StatusCode , err ) , res )
}
return "" , "" , utils . NewHTTPRequestError ( fmt . Sprintf ( "Received HTTP code %d while uploading layer: %s" , res . StatusCode , errBody ) , res )
}
checksumPayload = "sha256:" + hex . EncodeToString ( h . Sum ( nil ) )
return tarsumLayer . Sum ( jsonRaw ) , checksumPayload , nil
}
// push a tag on the registry.
// Remote has the format '<user>/<repo>
func ( r * Session ) PushRegistryTag ( remote , revision , tag , registry string , token [ ] string ) error {
// "jsonify" the string
revision = "\"" + revision + "\""
path := fmt . Sprintf ( "repositories/%s/tags/%s" , remote , tag )
req , err := r . reqFactory . NewRequest ( "PUT" , registry + path , strings . NewReader ( revision ) )
if err != nil {
return err
}
req . Header . Add ( "Content-type" , "application/json" )
setTokenAuth ( req , token )
req . ContentLength = int64 ( len ( revision ) )
res , _ , err := r . doRequest ( req )
if err != nil {
return err
}
res . Body . Close ( )
if res . StatusCode != 200 && res . StatusCode != 201 {
return utils . NewHTTPRequestError ( fmt . Sprintf ( "Internal server error: %d trying to push tag %s on %s" , res . StatusCode , tag , remote ) , res )
}
return nil
}
func ( r * Session ) PushImageJSONIndex ( remote string , imgList [ ] * ImgData , validate bool , regs [ ] string ) ( * RepositoryData , error ) {
cleanImgList := [ ] * ImgData { }
indexEp := r . indexEndpoint
if validate {
for _ , elem := range imgList {
if elem . Checksum != "" {
cleanImgList = append ( cleanImgList , elem )
}
}
} else {
cleanImgList = imgList
}
imgListJSON , err := json . Marshal ( cleanImgList )
if err != nil {
return nil , err
}
var suffix string
if validate {
suffix = "images"
}
u := fmt . Sprintf ( "%srepositories/%s/%s" , indexEp , remote , suffix )
2014-08-13 22:13:21 +00:00
log . Debugf ( "[registry] PUT %s" , u )
log . Debugf ( "Image list pushed to index:\n%s" , imgListJSON )
2014-08-07 14:43:06 +00:00
req , err := r . reqFactory . NewRequest ( "PUT" , u , bytes . NewReader ( imgListJSON ) )
if err != nil {
return nil , err
}
req . Header . Add ( "Content-type" , "application/json" )
req . SetBasicAuth ( r . authConfig . Username , r . authConfig . Password )
req . ContentLength = int64 ( len ( imgListJSON ) )
req . Header . Set ( "X-Docker-Token" , "true" )
if validate {
req . Header [ "X-Docker-Endpoints" ] = regs
}
res , _ , err := r . doRequest ( req )
if err != nil {
return nil , err
}
defer res . Body . Close ( )
// Redirect if necessary
for res . StatusCode >= 300 && res . StatusCode < 400 {
2014-08-13 22:13:21 +00:00
log . Debugf ( "Redirected to %s" , res . Header . Get ( "Location" ) )
2014-08-07 14:43:06 +00:00
req , err = r . reqFactory . NewRequest ( "PUT" , res . Header . Get ( "Location" ) , bytes . NewReader ( imgListJSON ) )
if err != nil {
return nil , err
}
req . SetBasicAuth ( r . authConfig . Username , r . authConfig . Password )
req . ContentLength = int64 ( len ( imgListJSON ) )
req . Header . Set ( "X-Docker-Token" , "true" )
if validate {
req . Header [ "X-Docker-Endpoints" ] = regs
}
res , _ , err := r . doRequest ( req )
if err != nil {
return nil , err
}
defer res . Body . Close ( )
}
var tokens , endpoints [ ] string
if ! validate {
if res . StatusCode != 200 && res . StatusCode != 201 {
errBody , err := ioutil . ReadAll ( res . Body )
if err != nil {
return nil , err
}
return nil , utils . NewHTTPRequestError ( fmt . Sprintf ( "Error: Status %d trying to push repository %s: %s" , res . StatusCode , remote , errBody ) , res )
}
if res . Header . Get ( "X-Docker-Token" ) != "" {
tokens = res . Header [ "X-Docker-Token" ]
2014-08-13 22:13:21 +00:00
log . Debugf ( "Auth token: %v" , tokens )
2014-08-07 14:43:06 +00:00
} else {
return nil , fmt . Errorf ( "Index response didn't contain an access token" )
}
if res . Header . Get ( "X-Docker-Endpoints" ) != "" {
endpoints , err = buildEndpointsList ( res . Header [ "X-Docker-Endpoints" ] , indexEp )
if err != nil {
return nil , err
}
} else {
return nil , fmt . Errorf ( "Index response didn't contain any endpoints" )
}
}
if validate {
if res . StatusCode != 204 {
errBody , err := ioutil . ReadAll ( res . Body )
if err != nil {
return nil , err
}
return nil , utils . NewHTTPRequestError ( fmt . Sprintf ( "Error: Status %d trying to push checksums %s: %s" , res . StatusCode , remote , errBody ) , res )
}
}
return & RepositoryData {
Tokens : tokens ,
Endpoints : endpoints ,
} , nil
}
func ( r * Session ) SearchRepositories ( term string ) ( * SearchResults , error ) {
2014-08-13 22:13:21 +00:00
log . Debugf ( "Index server: %s" , r . indexEndpoint )
2014-08-07 14:43:06 +00:00
u := r . indexEndpoint + "search?q=" + url . QueryEscape ( term )
req , err := r . reqFactory . NewRequest ( "GET" , u , nil )
if err != nil {
return nil , err
}
if r . authConfig != nil && len ( r . authConfig . Username ) > 0 {
req . SetBasicAuth ( r . authConfig . Username , r . authConfig . Password )
}
req . Header . Set ( "X-Docker-Token" , "true" )
res , _ , err := r . doRequest ( req )
if err != nil {
return nil , err
}
defer res . Body . Close ( )
if res . StatusCode != 200 {
return nil , utils . NewHTTPRequestError ( fmt . Sprintf ( "Unexepected status code %d" , res . StatusCode ) , res )
}
rawData , err := ioutil . ReadAll ( res . Body )
if err != nil {
return nil , err
}
result := new ( SearchResults )
err = json . Unmarshal ( rawData , result )
return result , err
}
func ( r * Session ) GetAuthConfig ( withPasswd bool ) * AuthConfig {
password := ""
if withPasswd {
password = r . authConfig . Password
}
return & AuthConfig {
Username : r . authConfig . Username ,
Password : password ,
Email : r . authConfig . Email ,
}
}
func setTokenAuth ( req * http . Request , token [ ] string ) {
if req . Header . Get ( "Authorization" ) == "" { // Don't override
req . Header . Set ( "Authorization" , "Token " + strings . Join ( token , "," ) )
}
}