diff --git a/docs/storage-drivers/s3.md b/docs/storage-drivers/s3.md
index 5b172f9e3..a59c93081 100644
--- a/docs/storage-drivers/s3.md
+++ b/docs/storage-drivers/s3.md
@@ -30,7 +30,7 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Amaz
Your AWS Access Key.
-
+
secretkey
|
@@ -41,7 +41,7 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Amaz
Your AWS Secret Key.
-
+
region
|
@@ -64,7 +64,7 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Amaz
The bucket name in which you want to store the registry's data.
-
+
encrypt
|
@@ -76,7 +76,7 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Amaz
not. A boolean value. The default is false.
-
+
secure
|
@@ -88,7 +88,7 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Amaz
default is true
.
-
+
v4auth
|
@@ -101,7 +101,7 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Amaz
false
.
-
+
chunksize
|
@@ -113,7 +113,7 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Amaz
should be a number that is larger than 5*1024*1024.
-
+
rootdirectory
|
@@ -124,6 +124,17 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Amaz
This is a prefix that will be applied to all S3 keys to allow you to segment data in your bucket if necessary.
+
+
+ storageclass
+ |
+
+ no
+ |
+
+ The S3 storage class applied to each registry file. The default value is STANDARD.
+ |
+
@@ -147,6 +158,8 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Amaz
`rootdirectory`: (optional) The root directory tree in which all registry files will be stored. Defaults to the empty string (bucket root).
+`storageclass`: (optional) The storage class applied to each registry file. Defaults to STANDARD. Valid options are STANDARD and REDUCED_REDUNDANCY.
+
# CloudFront as Middleware with S3 backend
## Use Case
diff --git a/registry/storage/driver/s3/s3.go b/registry/storage/driver/s3/s3.go
index f09e5508f..83fd74f71 100644
--- a/registry/storage/driver/s3/s3.go
+++ b/registry/storage/driver/s3/s3.go
@@ -1,7 +1,7 @@
// Package s3 provides a storagedriver.StorageDriver implementation to
// store blobs in Amazon S3 cloud storage.
//
-// This package leverages the AdRoll/goamz client library for interfacing with
+// This package leverages the docker/goamz client library for interfacing with
// s3.
//
// Because s3 is a key, value store the Stat call does not support last modification
@@ -59,6 +59,7 @@ type DriverParameters struct {
V4Auth bool
ChunkSize int64
RootDirectory string
+ StorageClass s3.StorageClass
UserAgent string
}
@@ -79,6 +80,7 @@ type driver struct {
ChunkSize int64
Encrypt bool
RootDirectory string
+ StorageClass s3.StorageClass
pool sync.Pool // pool []byte buffers used for WriteStream
zeros []byte // shared, zero-valued buffer used for WriteStream
@@ -183,6 +185,21 @@ func FromParameters(parameters map[string]interface{}) (*Driver, error) {
rootDirectory = ""
}
+ storageClass := s3.StandardStorage
+ storageClassParam, ok := parameters["storageclass"]
+ if ok {
+ storageClassString, ok := storageClassParam.(string)
+ if !ok {
+ return nil, fmt.Errorf("The storageclass parameter must be one of %v, %v invalid", []s3.StorageClass{s3.StandardStorage, s3.ReducedRedundancy}, storageClassParam)
+ }
+ // All valid storage class parameters are UPPERCASE, so be a bit more flexible here
+ storageClassCasted := s3.StorageClass(strings.ToUpper(storageClassString))
+ if storageClassCasted != s3.StandardStorage && storageClassCasted != s3.ReducedRedundancy {
+ return nil, fmt.Errorf("The storageclass parameter must be one of %v, %v invalid", []s3.StorageClass{s3.StandardStorage, s3.ReducedRedundancy}, storageClassParam)
+ }
+ storageClass = storageClassCasted
+ }
+
userAgent, ok := parameters["useragent"]
if !ok {
userAgent = ""
@@ -198,6 +215,7 @@ func FromParameters(parameters map[string]interface{}) (*Driver, error) {
v4AuthBool,
chunkSize,
fmt.Sprint(rootDirectory),
+ storageClass,
fmt.Sprint(userAgent),
}
@@ -259,6 +277,7 @@ func New(params DriverParameters) (*Driver, error) {
ChunkSize: params.ChunkSize,
Encrypt: params.Encrypt,
RootDirectory: params.RootDirectory,
+ StorageClass: params.StorageClass,
zeros: make([]byte, params.ChunkSize),
}
@@ -826,7 +845,10 @@ func hasCode(err error, code string) bool {
}
func (d *driver) getOptions() s3.Options {
- return s3.Options{SSE: d.Encrypt}
+ return s3.Options{
+ SSE: d.Encrypt,
+ StorageClass: d.StorageClass,
+ }
}
func getPermissions() s3.ACL {
diff --git a/registry/storage/driver/s3/s3_test.go b/registry/storage/driver/s3/s3_test.go
index 86f433f31..660d5350b 100644
--- a/registry/storage/driver/s3/s3_test.go
+++ b/registry/storage/driver/s3/s3_test.go
@@ -10,6 +10,7 @@ import (
storagedriver "github.com/docker/distribution/registry/storage/driver"
"github.com/docker/distribution/registry/storage/driver/testsuites"
"github.com/docker/goamz/aws"
+ "github.com/docker/goamz/s3"
"gopkg.in/check.v1"
)
@@ -17,7 +18,7 @@ import (
// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { check.TestingT(t) }
-var s3DriverConstructor func(rootDirectory string) (*Driver, error)
+var s3DriverConstructor func(rootDirectory string, storageClass s3.StorageClass) (*Driver, error)
var skipS3 func() string
func init() {
@@ -34,7 +35,7 @@ func init() {
}
defer os.Remove(root)
- s3DriverConstructor = func(rootDirectory string) (*Driver, error) {
+ s3DriverConstructor = func(rootDirectory string, storageClass s3.StorageClass) (*Driver, error) {
encryptBool := false
if encrypt != "" {
encryptBool, err = strconv.ParseBool(encrypt)
@@ -69,6 +70,7 @@ func init() {
v4AuthBool,
minChunkSize,
rootDirectory,
+ storageClass,
"",
}
@@ -84,7 +86,7 @@ func init() {
}
testsuites.RegisterSuite(func() (storagedriver.StorageDriver, error) {
- return s3DriverConstructor(root)
+ return s3DriverConstructor(root, s3.StandardStorage)
}, skipS3)
}
@@ -99,17 +101,17 @@ func TestEmptyRootList(t *testing.T) {
}
defer os.Remove(validRoot)
- rootedDriver, err := s3DriverConstructor(validRoot)
+ rootedDriver, err := s3DriverConstructor(validRoot, s3.StandardStorage)
if err != nil {
t.Fatalf("unexpected error creating rooted driver: %v", err)
}
- emptyRootDriver, err := s3DriverConstructor("")
+ emptyRootDriver, err := s3DriverConstructor("", s3.StandardStorage)
if err != nil {
t.Fatalf("unexpected error creating empty root driver: %v", err)
}
- slashRootDriver, err := s3DriverConstructor("/")
+ slashRootDriver, err := s3DriverConstructor("/", s3.StandardStorage)
if err != nil {
t.Fatalf("unexpected error creating slash root driver: %v", err)
}
@@ -137,3 +139,63 @@ func TestEmptyRootList(t *testing.T) {
}
}
}
+
+func TestStorageClass(t *testing.T) {
+ if skipS3() != "" {
+ t.Skip(skipS3())
+ }
+
+ rootDir, err := ioutil.TempDir("", "driver-")
+ if err != nil {
+ t.Fatalf("unexpected error creating temporary directory: %v", err)
+ }
+ defer os.Remove(rootDir)
+
+ standardDriver, err := s3DriverConstructor(rootDir, s3.StandardStorage)
+ if err != nil {
+ t.Fatalf("unexpected error creating driver with standard storage: %v", err)
+ }
+
+ rrDriver, err := s3DriverConstructor(rootDir, s3.ReducedRedundancy)
+ if err != nil {
+ t.Fatalf("unexpected error creating driver with reduced redundancy storage: %v", err)
+ }
+
+ standardFilename := "/test-standard"
+ rrFilename := "/test-rr"
+ contents := []byte("contents")
+ ctx := context.Background()
+
+ err = standardDriver.PutContent(ctx, standardFilename, contents)
+ if err != nil {
+ t.Fatalf("unexpected error creating content: %v", err)
+ }
+ defer standardDriver.Delete(ctx, standardFilename)
+
+ err = rrDriver.PutContent(ctx, rrFilename, contents)
+ if err != nil {
+ t.Fatalf("unexpected error creating content: %v", err)
+ }
+ defer rrDriver.Delete(ctx, rrFilename)
+
+ standardDriverUnwrapped := standardDriver.Base.StorageDriver.(*driver)
+ resp, err := standardDriverUnwrapped.Bucket.GetResponse(standardDriverUnwrapped.s3Path(standardFilename))
+ if err != nil {
+ t.Fatalf("unexpected error retrieving standard storage file: %v", err)
+ }
+ defer resp.Body.Close()
+ // Amazon only populates this header value for non-standard storage classes
+ if storageClass := resp.Header.Get("x-amz-storage-class"); storageClass != "" {
+ t.Fatalf("unexpected storage class for standard file: %v", storageClass)
+ }
+
+ rrDriverUnwrapped := rrDriver.Base.StorageDriver.(*driver)
+ resp, err = rrDriverUnwrapped.Bucket.GetResponse(rrDriverUnwrapped.s3Path(rrFilename))
+ if err != nil {
+ t.Fatalf("unexpected error retrieving reduced-redundancy storage file: %v", err)
+ }
+ defer resp.Body.Close()
+ if storageClass := resp.Header.Get("x-amz-storage-class"); storageClass != string(s3.ReducedRedundancy) {
+ t.Fatalf("unexpected storage class for standard file: %v", storageClass)
+ }
+}