diff --git a/configuration/configuration.go b/configuration/configuration.go index 8bd46407..4ff339b8 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -24,6 +24,9 @@ type Configuration struct { // used to gate requests. Auth Auth `yaml:"auth"` + // LayerHandler specifies a middleware for serving image layers. + LayerHandler LayerHandler `yaml:"layerhandler"` + // Reporting is the configuration for error reporting Reporting Reporting `yaml:"reporting"` @@ -240,6 +243,62 @@ 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 { + t := layerHandler.Type() + if t == "" { + return nil, nil + } + return t, 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..72ac4f06 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.LayerHandler.Type() + + if layerHandlerType != "" { + lh, err := storage.GetLayerHandler(layerHandlerType, configuration.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..836df3b7 100644 --- a/registry/layer.go +++ b/registry/layer.go @@ -58,5 +58,13 @@ func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) { } defer layer.Close() + if lh.layerHandler != nil { + handler, _ := 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..3afc4c0e --- /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(20*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..cc0622bf --- /dev/null +++ b/storage/delegatelayerhandler.go @@ -0,0 +1,73 @@ +package storage + +import ( + "fmt" + "net/http" + "time" + + "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 + duration time.Duration +} + +var _ LayerHandler = &delegateLayerHandler{} + +func newDelegateLayerHandler(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (LayerHandler, error) { + duration := 20 * time.Minute + d, ok := options["duration"] + if ok { + switch d := d.(type) { + case time.Duration: + duration = d + case string: + dur, err := time.ParseDuration(d) + if err != nil { + return nil, fmt.Errorf("Invalid duration: %s", err) + } + duration = dur + } + } + + return &delegateLayerHandler{storageDriver: storageDriver, pathMapper: defaultPathMapper, duration: duration}, 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 := resolveBlobPath(lh.storageDriver, lh.pathMapper, layer.Name(), layer.Digest()) + if err != nil { + return "", err + } + + layerURL, err := lh.storageDriver.URLFor(blobPath, map[string]interface{}{"expiry": time.Now().Add(lh.duration)}) + if err != nil { + return "", err + } + + return layerURL, nil +} + +// 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..f73bef6d 100644 --- a/storage/layerstore.go +++ b/storage/layerstore.go @@ -30,7 +30,7 @@ func (ls *layerStore) Exists(name string, digest digest.Digest) (bool, error) { } func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) { - blobPath, err := ls.resolveBlobPath(name, digest) + blobPath, err := resolveBlobPath(ls.driver, ls.pathMapper, name, digest) if err != nil { switch err := err.(type) { case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError: @@ -94,31 +94,3 @@ func (ls *layerStore) newLayerUpload(lus LayerUploadState) LayerUpload { uploadStore: ls.uploadStore, } } - -// resolveBlobId 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} - layerLinkPath, err := ls.pathMapper.path(pathSpec) - - if err != nil { - return "", err - } - - layerLinkContent, err := ls.driver.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 ls.pathMapper.path(bp) -} diff --git a/storage/paths.go b/storage/paths.go index a19f2d70..c5d6c90f 100644 --- a/storage/paths.go +++ b/storage/paths.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/docker/distribution/digest" + "github.com/docker/distribution/storagedriver" ) const storagePathVersion = "v2" @@ -44,6 +45,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) { @@ -204,3 +210,31 @@ func digestPathComoponents(dgst digest.Digest) ([]string, error) { return append(prefix, suffix...), 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 resolveBlobPath(driver storagedriver.StorageDriver, pm *pathMapper, name string, dgst digest.Digest) (string, error) { + pathSpec := layerLinkPathSpec{name: name, digest: dgst} + layerLinkPath, err := pm.path(pathSpec) + + if err != nil { + return "", err + } + + layerLinkContent, err := driver.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 pm.path(bp) +} 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, } } diff --git a/storagedriver/filesystem/driver.go b/storagedriver/filesystem/driver.go index cf52b383..2de7c163 100644 --- a/storagedriver/filesystem/driver.go +++ b/storagedriver/filesystem/driver.go @@ -267,7 +267,7 @@ func (d *Driver) Delete(subPath string) error { // URLFor returns a URL which may be used to retrieve the content stored at the given path. // May return an UnsupportedMethodErr in certain StorageDriver implementations. -func (d *Driver) URLFor(path string) (string, error) { +func (d *Driver) URLFor(path string, options map[string]interface{}) (string, error) { return "", storagedriver.ErrUnsupportedMethod } diff --git a/storagedriver/inmemory/driver.go b/storagedriver/inmemory/driver.go index 89692623..e3c63f74 100644 --- a/storagedriver/inmemory/driver.go +++ b/storagedriver/inmemory/driver.go @@ -254,6 +254,6 @@ func (d *Driver) Delete(path string) error { // URLFor returns a URL which may be used to retrieve the content stored at the given path. // May return an UnsupportedMethodErr in certain StorageDriver implementations. -func (d *Driver) URLFor(path string) (string, error) { +func (d *Driver) URLFor(path string, options map[string]interface{}) (string, error) { return "", storagedriver.ErrUnsupportedMethod } diff --git a/storagedriver/s3/s3.go b/storagedriver/s3/s3.go index 916a4287..0e4dd216 100644 --- a/storagedriver/s3/s3.go +++ b/storagedriver/s3/s3.go @@ -582,12 +582,21 @@ func (d *Driver) Delete(path string) error { // URLFor returns a URL which may be used to retrieve the content stored at the given path. // May return an UnsupportedMethodErr in certain StorageDriver implementations. -func (d *Driver) URLFor(path string) (string, error) { +func (d *Driver) URLFor(path string, options map[string]interface{}) (string, error) { if !storagedriver.PathRegexp.MatchString(path) { return "", storagedriver.InvalidPathError{Path: path} } - return d.Bucket.SignedURL(d.s3Path(path), time.Now().Add(24*time.Hour)), nil + expiresTime := time.Now().Add(20 * time.Minute) + expires, ok := options["expiry"] + if ok { + et, ok := expires.(time.Time) + if ok { + expiresTime = et + } + } + + return d.Bucket.SignedURL(d.s3Path(path), expiresTime), nil } func (d *Driver) s3Path(path string) string { diff --git a/storagedriver/storagedriver.go b/storagedriver/storagedriver.go index f5a3ca00..d050d68f 100644 --- a/storagedriver/storagedriver.go +++ b/storagedriver/storagedriver.go @@ -71,9 +71,11 @@ type StorageDriver interface { // Delete recursively deletes all objects stored at "path" and its subpaths. Delete(path string) error - // URLFor returns a URL which may be used to retrieve the content stored at the given path. - // May return an UnsupportedMethodErr in certain StorageDriver implementations. - URLFor(path string) (string, error) + // URLFor returns a URL which may be used to retrieve the content stored at + // the given path, possibly using the given options. + // May return an UnsupportedMethodErr in certain StorageDriver + // implementations. + URLFor(path string, options map[string]interface{}) (string, error) } // PathRegexp is the regular expression which each file path must match. diff --git a/storagedriver/testsuites/testsuites.go b/storagedriver/testsuites/testsuites.go index 63849019..486640c4 100644 --- a/storagedriver/testsuites/testsuites.go +++ b/storagedriver/testsuites/testsuites.go @@ -592,7 +592,7 @@ func (suite *DriverSuite) TestURLFor(c *check.C) { err := suite.StorageDriver.PutContent(filename, contents) c.Assert(err, check.IsNil) - url, err := suite.StorageDriver.URLFor(filename) + url, err := suite.StorageDriver.URLFor(filename, nil) if err == storagedriver.ErrUnsupportedMethod { return }