From 17915e1b013391442aba0c215ce0b96171e34882 Mon Sep 17 00:00:00 2001 From: Brian Bland Date: Thu, 8 Jan 2015 16:55:40 -0800 Subject: [PATCH] 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 --- configuration/configuration.go | 55 ++++++++++++++++ registry/app.go | 12 ++++ registry/layer.go | 6 ++ storage/cloudfrontlayerhandler.go | 106 ++++++++++++++++++++++++++++++ storage/delegatelayerhandler.go | 84 +++++++++++++++++++++++ storage/layerhandler.go | 50 ++++++++++++++ storage/layerstore.go | 2 +- storage/paths.go | 5 ++ storage/services.go | 7 +- 9 files changed, 321 insertions(+), 6 deletions(-) create mode 100644 storage/cloudfrontlayerhandler.go create mode 100644 storage/delegatelayerhandler.go create mode 100644 storage/layerhandler.go diff --git a/configuration/configuration.go b/configuration/configuration.go index 8bd46407..06aa4221 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -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 // diff --git a/registry/app.go b/registry/app.go index fefeb084..b757b9ab 100644 --- a/registry/app.go +++ b/registry/app.go @@ -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 } diff --git a/registry/layer.go b/registry/layer.go index a7c46c31..5d43a1ad 100644 --- a/registry/layer.go +++ b/registry/layer.go @@ -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) } diff --git a/storage/cloudfrontlayerhandler.go b/storage/cloudfrontlayerhandler.go new file mode 100644 index 00000000..703c0f21 --- /dev/null +++ b/storage/cloudfrontlayerhandler.go @@ -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)) +} diff --git a/storage/delegatelayerhandler.go b/storage/delegatelayerhandler.go new file mode 100644 index 00000000..34c8315b --- /dev/null +++ b/storage/delegatelayerhandler.go @@ -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)) +} diff --git a/storage/layerhandler.go b/storage/layerhandler.go new file mode 100644 index 00000000..2755470e --- /dev/null +++ b/storage/layerhandler.go @@ -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) +} diff --git a/storage/layerstore.go b/storage/layerstore.go index 42bd0f4f..6b351454 100644 --- a/storage/layerstore.go +++ b/storage/layerstore.go @@ -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} diff --git a/storage/paths.go b/storage/paths.go index a19f2d70..695bfa39 100644 --- a/storage/paths.go +++ b/storage/paths.go @@ -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) { diff --git a/storage/services.go b/storage/services.go index 15008f84..5507faeb 100644 --- a/storage/services.go +++ b/storage/services.go @@ -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, } }