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 #49master
							parent
							
								
									65863802d7
								
							
						
					
					
						commit
						17915e1b01
					
				|  | @ -35,6 +35,9 @@ type Configuration struct { | |||
| 
 | ||||
| 		// Secret specifies the secret key which HMAC tokens are created with.
 | ||||
| 		Secret string `yaml:"secret"` | ||||
| 
 | ||||
| 		// LayerHandler specifies a middleware for serving image layers.
 | ||||
| 		LayerHandler LayerHandler `yaml:"layerhandler"` | ||||
| 	} `yaml:"http"` | ||||
| } | ||||
| 
 | ||||
|  | @ -240,6 +243,58 @@ type NewRelicReporting struct { | |||
| 	Name string `yaml:"name"` | ||||
| } | ||||
| 
 | ||||
| // LayerHandler defines the configuration for middleware layer serving
 | ||||
| type LayerHandler map[string]Parameters | ||||
| 
 | ||||
| // Type returns the layerhandler type
 | ||||
| func (layerHandler LayerHandler) Type() string { | ||||
| 	// Return only key in this map
 | ||||
| 	for k := range layerHandler { | ||||
| 		return k | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| // Parameters returns the Parameters map for a LayerHandler configuration
 | ||||
| func (layerHandler LayerHandler) Parameters() Parameters { | ||||
| 	return layerHandler[layerHandler.Type()] | ||||
| } | ||||
| 
 | ||||
| // UnmarshalYAML implements the yaml.Unmarshaler interface
 | ||||
| // Unmarshals a single item map into a Storage or a string into a Storage type with no parameters
 | ||||
| func (layerHandler *LayerHandler) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var storageMap map[string]Parameters | ||||
| 	err := unmarshal(&storageMap) | ||||
| 	if err == nil { | ||||
| 		if len(storageMap) > 1 { | ||||
| 			types := make([]string, 0, len(storageMap)) | ||||
| 			for k := range storageMap { | ||||
| 				types = append(types, k) | ||||
| 			} | ||||
| 			return fmt.Errorf("Must provide exactly one layerhandler type. Provided: %v", types) | ||||
| 		} | ||||
| 		*layerHandler = storageMap | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	var storageType string | ||||
| 	err = unmarshal(&storageType) | ||||
| 	if err == nil { | ||||
| 		*layerHandler = LayerHandler{storageType: Parameters{}} | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // MarshalYAML implements the yaml.Marshaler interface
 | ||||
| func (layerHandler LayerHandler) MarshalYAML() (interface{}, error) { | ||||
| 	if layerHandler.Parameters() == nil { | ||||
| 		return layerHandler.Type(), nil | ||||
| 	} | ||||
| 	return map[string]Parameters(layerHandler), nil | ||||
| } | ||||
| 
 | ||||
| // Parse parses an input configuration yaml document into a Configuration struct
 | ||||
| // This should generally be capable of handling old configuration format versions
 | ||||
| //
 | ||||
|  |  | |||
|  | @ -31,6 +31,8 @@ type App struct { | |||
| 
 | ||||
| 	tokenProvider tokenProvider | ||||
| 
 | ||||
| 	layerHandler storage.LayerHandler | ||||
| 
 | ||||
| 	accessController auth.AccessController | ||||
| } | ||||
| 
 | ||||
|  | @ -76,6 +78,16 @@ func NewApp(configuration configuration.Configuration) *App { | |||
| 		app.accessController = accessController | ||||
| 	} | ||||
| 
 | ||||
| 	layerHandlerType := configuration.HTTP.LayerHandler.Type() | ||||
| 
 | ||||
| 	if layerHandlerType != "" { | ||||
| 		lh, err := storage.GetLayerHandler(layerHandlerType, configuration.HTTP.LayerHandler.Parameters(), driver) | ||||
| 		if err != nil { | ||||
| 			panic(fmt.Sprintf("unable to configure layer handler (%s): %v", layerHandlerType, err)) | ||||
| 		} | ||||
| 		app.layerHandler = lh | ||||
| 	} | ||||
| 
 | ||||
| 	return app | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -58,5 +58,11 @@ func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) { | |||
| 	} | ||||
| 	defer layer.Close() | ||||
| 
 | ||||
| 	handler, err := lh.layerHandler.Resolve(layer) | ||||
| 	if handler != nil { | ||||
| 		handler.ServeHTTP(w, r) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	http.ServeContent(w, r, layer.Digest().String(), layer.CreatedAt(), layer) | ||||
| } | ||||
|  |  | |||
|  | @ -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)) | ||||
| } | ||||
|  | @ -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)) | ||||
| } | ||||
|  | @ -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) | ||||
| } | ||||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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) { | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
| 	} | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue