Adds support for content redirects for layer downloads

Includes a delegate implementation which redirects to the URL generated
by the storagedriver, and a cloudfront implementation.
Satisfies proposal #49
This commit is contained in:
Brian Bland 2015-01-08 16:55:40 -08:00
parent 65863802d7
commit 17915e1b01
9 changed files with 321 additions and 6 deletions

View file

@ -0,0 +1,106 @@
package storage
import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/crowdmob/goamz/cloudfront"
"github.com/docker/distribution/storagedriver"
)
// cloudFrontLayerHandler provides an simple implementation of layerHandler that
// constructs temporary signed CloudFront URLs from the storagedriver layer URL,
// then issues HTTP Temporary Redirects to this CloudFront content URL.
type cloudFrontLayerHandler struct {
cloudfront *cloudfront.CloudFront
delegateLayerHandler *delegateLayerHandler
}
var _ LayerHandler = &cloudFrontLayerHandler{}
// newCloudFrontLayerHandler constructs and returns a new CloudFront
// LayerHandler implementation.
// Required options: baseurl, privatekey, keypairid
func newCloudFrontLayerHandler(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (LayerHandler, error) {
base, ok := options["baseurl"]
if !ok {
return nil, fmt.Errorf("No baseurl provided")
}
baseURL, ok := base.(string)
if !ok {
return nil, fmt.Errorf("baseurl must be a string")
}
pk, ok := options["privatekey"]
if !ok {
return nil, fmt.Errorf("No privatekey provided")
}
pkPath, ok := pk.(string)
if !ok {
return nil, fmt.Errorf("privatekey must be a string")
}
kpid, ok := options["keypairid"]
if !ok {
return nil, fmt.Errorf("No keypairid provided")
}
keypairID, ok := kpid.(string)
if !ok {
return nil, fmt.Errorf("keypairid must be a string")
}
pkBytes, err := ioutil.ReadFile(pkPath)
if err != nil {
return nil, fmt.Errorf("Failed to read privatekey file: %s", err)
}
block, _ := pem.Decode([]byte(pkBytes))
if block == nil {
return nil, fmt.Errorf("Failed to decode private key as an rsa private key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
lh, err := newDelegateLayerHandler(storageDriver, options)
if err != nil {
return nil, err
}
dlh := lh.(*delegateLayerHandler)
cf := cloudfront.New(baseURL, privateKey, keypairID)
return &cloudFrontLayerHandler{cloudfront: cf, delegateLayerHandler: dlh}, nil
}
// Resolve returns an http.Handler which can serve the contents of the given
// Layer, or an error if not supported by the storagedriver.
func (lh *cloudFrontLayerHandler) Resolve(layer Layer) (http.Handler, error) {
layerURLStr, err := lh.delegateLayerHandler.urlFor(layer)
if err != nil {
return nil, err
}
layerURL, err := url.Parse(layerURLStr)
if err != nil {
return nil, err
}
cfURL, err := lh.cloudfront.CannedSignedURL(layerURL.Path, "", time.Now().Add(time.Minute))
if err != nil {
return nil, err
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, cfURL, http.StatusTemporaryRedirect)
}), nil
}
// init registers the cloudfront layerHandler backend.
func init() {
RegisterLayerHandler("cloudfront", LayerHandlerInitFunc(newCloudFrontLayerHandler))
}

View file

@ -0,0 +1,84 @@
package storage
import (
"net/http"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/storagedriver"
)
// delegateLayerHandler provides a simple implementation of layerHandler that
// simply issues HTTP Temporary Redirects to the URL provided by the
// storagedriver for a given Layer.
type delegateLayerHandler struct {
storageDriver storagedriver.StorageDriver
pathMapper *pathMapper
}
var _ LayerHandler = &delegateLayerHandler{}
func newDelegateLayerHandler(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (LayerHandler, error) {
return &delegateLayerHandler{storageDriver: storageDriver, pathMapper: defaultPathMapper}, nil
}
// Resolve returns an http.Handler which can serve the contents of the given
// Layer, or an error if not supported by the storagedriver.
func (lh *delegateLayerHandler) Resolve(layer Layer) (http.Handler, error) {
layerURL, err := lh.urlFor(layer)
if err != nil {
return nil, err
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, layerURL, http.StatusTemporaryRedirect)
}), nil
}
// urlFor returns a download URL for the given layer, or the empty string if
// unsupported.
func (lh *delegateLayerHandler) urlFor(layer Layer) (string, error) {
blobPath, err := lh.resolveBlobPath(layer.Name(), layer.Digest())
if err != nil {
return "", err
}
layerURL, err := lh.storageDriver.URLFor(blobPath)
if err != nil {
return "", err
}
return layerURL, nil
}
// resolveBlobPath looks up the blob location in the repositories from a
// layer/blob link file, returning blob path or an error on failure.
func (lh *delegateLayerHandler) resolveBlobPath(name string, dgst digest.Digest) (string, error) {
pathSpec := layerLinkPathSpec{name: name, digest: dgst}
layerLinkPath, err := lh.pathMapper.path(pathSpec)
if err != nil {
return "", err
}
layerLinkContent, err := lh.storageDriver.GetContent(layerLinkPath)
if err != nil {
return "", err
}
// NOTE(stevvooe): The content of the layer link should match the digest.
// This layer of indirection is for name-based content protection.
linked, err := digest.ParseDigest(string(layerLinkContent))
if err != nil {
return "", err
}
bp := blobPathSpec{digest: linked}
return lh.pathMapper.path(bp)
}
// init registers the delegate layerHandler backend.
func init() {
RegisterLayerHandler("delegate", LayerHandlerInitFunc(newDelegateLayerHandler))
}

50
storage/layerhandler.go Normal file
View file

@ -0,0 +1,50 @@
package storage
import (
"fmt"
"net/http"
"github.com/docker/distribution/storagedriver"
)
// LayerHandler provides middleware for serving the contents of a Layer.
type LayerHandler interface {
// Resolve returns an http.Handler which can serve the contents of a given
// Layer if possible, or nil and an error when unsupported. This may
// directly serve the contents of the layer or issue a redirect to another
// URL hosting the content.
Resolve(layer Layer) (http.Handler, error)
}
// LayerHandlerInitFunc is the type of a LayerHandler factory function and is
// used to register the contsructor for different LayerHandler backends.
type LayerHandlerInitFunc func(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (LayerHandler, error)
var layerHandlers map[string]LayerHandlerInitFunc
// RegisterLayerHandler is used to register an LayerHandlerInitFunc for
// a LayerHandler backend with the given name.
func RegisterLayerHandler(name string, initFunc LayerHandlerInitFunc) error {
if layerHandlers == nil {
layerHandlers = make(map[string]LayerHandlerInitFunc)
}
if _, exists := layerHandlers[name]; exists {
return fmt.Errorf("name already registered: %s", name)
}
layerHandlers[name] = initFunc
return nil
}
// GetLayerHandler constructs a LayerHandler
// with the given options using the named backend.
func GetLayerHandler(name string, options map[string]interface{}, storageDriver storagedriver.StorageDriver) (LayerHandler, error) {
if layerHandlers != nil {
if initFunc, exists := layerHandlers[name]; exists {
return initFunc(storageDriver, options)
}
}
return nil, fmt.Errorf("no layer handler registered with name: %s", name)
}

View file

@ -95,7 +95,7 @@ func (ls *layerStore) newLayerUpload(lus LayerUploadState) LayerUpload {
}
}
// resolveBlobId looks up the blob location in the repositories from a
// resolveBlobPath looks up the blob location in the repositories from a
// layer/blob link file, returning blob path or an error on failure.
func (ls *layerStore) resolveBlobPath(name string, dgst digest.Digest) (string, error) {
pathSpec := layerLinkPathSpec{name: name, digest: dgst}

View file

@ -44,6 +44,11 @@ type pathMapper struct {
version string // should be a constant?
}
var defaultPathMapper = &pathMapper{
root: "/docker/registry/",
version: storagePathVersion,
}
// path returns the path identified by spec.
func (pm *pathMapper) path(spec pathSpec) (string, error) {

View file

@ -28,11 +28,8 @@ func NewServices(driver storagedriver.StorageDriver) *Services {
return &Services{
driver: driver,
pathMapper: &pathMapper{
// TODO(sday): This should be configurable.
root: "/docker/registry/",
version: storagePathVersion,
},
// TODO(sday): This should be configurable.
pathMapper: defaultPathMapper,
layerUploadStore: layerUploadStore,
}
}