mirror of
https://github.com/pgsty/minio.git
synced 2026-08-10 16:23:28 +03:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f14433cbd | |||
| 50a817e3d3 | |||
| 5a4a57700b | |||
| 3de5a3157f | |||
| 50dec08002 | |||
| e71ef905f9 | |||
| 3d197c1449 | |||
| 65de2d68c0 | |||
| 1103ad2d08 | |||
| eab947cf42 | |||
| 0fe9e95250 | |||
| c7946ab9ab | |||
| f5df3b4795 | |||
| f26325c988 | |||
| 7c14cdb60e | |||
| 0e02328c98 | |||
| 380524ae27 | |||
| 0286e61aee | |||
| ff29aed05d | |||
| 64f2c61813 | |||
| 525c04fd07 | |||
| efac90461a | |||
| 71979376b5 | |||
| 5a1ae862a7 | |||
| 6df20734f9 | |||
| 98bce72295 | |||
| 2f1756489e | |||
| 9719640e34 | |||
| ce02ab613d | |||
| 37de2dbd3b | |||
| a82500f162 | |||
| 2dede2fdc2 | |||
| 13fbb96736 | |||
| eabfcea34e | |||
| a078703214 | |||
| bd2b22572f | |||
| 5f69f04909 | |||
| a1a426e523 | |||
| a091b1a3ee | |||
| 556a51120c | |||
| eb391a53c1 | |||
| e17e09ea3c | |||
| 76c423392a | |||
| 8e6d756e3a | |||
| a7c9058375 | |||
| b16e33bcf5 | |||
| 197af49c99 | |||
| 264cc4020f | |||
| df88421087 | |||
| dbd89bbae3 | |||
| 224a272cf2 | |||
| ad86454580 | |||
| 644c2ce326 | |||
| 2debe77586 | |||
| 20480ba3f7 | |||
| 4f52d22c36 | |||
| cbe8df198e | |||
| 157ed65c35 | |||
| 869018ad14 | |||
| 5acc2a6db1 | |||
| 2cd14f567c | |||
| f1be356cc6 | |||
| 76ddf4d32f | |||
| 7b91bd71fe | |||
| 36ab615518 | |||
| 963a70053b | |||
| 9c5e971a58 | |||
| b1c9eb0e01 | |||
| b8f4f26cf6 | |||
| 43cc0096fa | |||
| e8a008f5b5 | |||
| 758a80e39b | |||
| 3ec4738955 | |||
| 6c93c60424 | |||
| 0c9f4c9092 | |||
| 289d6ce1d7 | |||
| 2a12e694f3 | |||
| db26d3c9e2 | |||
| 914c76a801 | |||
| a1ef90be52 | |||
| 7c4a41b933 | |||
| 2aa18cafc6 | |||
| adf7340394 | |||
| b11a8eb3f4 | |||
| 15771ebe8d | |||
| 44865596db | |||
| c9bc7e47b9 | |||
| be1700f595 | |||
| 42c5b64e4e | |||
| 40ed0d1f5d | |||
| b181a693fb | |||
| 4ddc222f46 | |||
| c310cbbe89 | |||
| 0ef0d7e685 | |||
| c62813c887 | |||
| 1da362538b | |||
| e40a5e05e1 | |||
| b0b0fb4c8d | |||
| 726e75611e | |||
| 317e648c0d | |||
| 80b3e9cb03 | |||
| 6c85706c24 | |||
| d7ced9a8b5 | |||
| 360f3f9335 | |||
| 25f9b0bc3b | |||
| a5453c307f | |||
| 92a6676a2f | |||
| f53d511798 |
@@ -22,3 +22,4 @@ parts/
|
|||||||
prime/
|
prime/
|
||||||
stage/
|
stage/
|
||||||
.sia_temp/
|
.sia_temp/
|
||||||
|
config.json
|
||||||
+14
-18
@@ -8,27 +8,23 @@ dist: trusty
|
|||||||
|
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
os:
|
matrix:
|
||||||
- linux
|
include:
|
||||||
|
- os: linux
|
||||||
env:
|
env:
|
||||||
- ARCH=x86_64
|
- ARCH=x86_64
|
||||||
|
go: 1.10.1
|
||||||
|
script:
|
||||||
|
- make
|
||||||
|
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||||
|
- diff -au <(gofmt -s -d pkg) <(printf "")
|
||||||
|
- make test GOFLAGS="-timeout 15m -race -v"
|
||||||
|
- make coverage
|
||||||
|
- node --version
|
||||||
|
- cd browser && yarn && yarn test && cd ..
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- nvm install stable
|
- nvm install stable
|
||||||
|
|
||||||
script:
|
|
||||||
## Run all the tests
|
|
||||||
- make
|
|
||||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
|
||||||
- diff -au <(gofmt -s -d pkg) <(printf "")
|
|
||||||
- make test GOFLAGS="-timeout 15m -race -v"
|
|
||||||
- make coverage
|
|
||||||
- node --version
|
|
||||||
- cd browser && yarn && yarn test && cd ..
|
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
go:
|
|
||||||
- '1.10.1'
|
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ ineffassign:
|
|||||||
|
|
||||||
cyclo:
|
cyclo:
|
||||||
@echo "Running $@"
|
@echo "Running $@"
|
||||||
@${GOPATH}/bin/gocyclo -over 100 cmd
|
@${GOPATH}/bin/gocyclo -over 200 cmd
|
||||||
@${GOPATH}/bin/gocyclo -over 100 pkg
|
@${GOPATH}/bin/gocyclo -over 200 pkg
|
||||||
|
|
||||||
deadcode:
|
deadcode:
|
||||||
@echo "Running $@"
|
@echo "Running $@"
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class ObjectActions extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { object, showShareObjectModal } = this.props
|
const { object, showShareObjectModal, shareObjectName } = this.props
|
||||||
return (
|
return (
|
||||||
<Dropdown id={`obj-actions-${object.name}`}>
|
<Dropdown id={`obj-actions-${object.name}`}>
|
||||||
<Dropdown.Toggle noCaret className="fia-toggle" />
|
<Dropdown.Toggle noCaret className="fia-toggle" />
|
||||||
@@ -77,7 +77,8 @@ export class ObjectActions extends React.Component {
|
|||||||
<i className="fa fa-trash" />
|
<i className="fa fa-trash" />
|
||||||
</a>
|
</a>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
{showShareObjectModal && <ShareObjectModal object={object} />}
|
{(showShareObjectModal && shareObjectName === object.name) &&
|
||||||
|
<ShareObjectModal object={object} />}
|
||||||
{this.state.showDeleteConfirmation && (
|
{this.state.showDeleteConfirmation && (
|
||||||
<DeleteObjectConfirmModal
|
<DeleteObjectConfirmModal
|
||||||
deleteObject={this.deleteObject.bind(this)}
|
deleteObject={this.deleteObject.bind(this)}
|
||||||
@@ -92,7 +93,8 @@ export class ObjectActions extends React.Component {
|
|||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
return {
|
return {
|
||||||
object: ownProps.object,
|
object: ownProps.object,
|
||||||
showShareObjectModal: state.objects.shareObject.show
|
showShareObjectModal: state.objects.shareObject.show,
|
||||||
|
shareObjectName: state.objects.shareObject.object
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,8 +88,21 @@ describe("ObjectActions", () => {
|
|||||||
object={{ name: "obj1" }}
|
object={{ name: "obj1" }}
|
||||||
currentPrefix={"pre1/"}
|
currentPrefix={"pre1/"}
|
||||||
showShareObjectModal={true}
|
showShareObjectModal={true}
|
||||||
|
shareObjectName={"obj1"}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(1)
|
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("shouldn't render ShareObjectModal when the names of the objects don't match", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<ObjectActions
|
||||||
|
object={{ name: "obj1" }}
|
||||||
|
currentPrefix={"pre1/"}
|
||||||
|
showShareObjectModal={true}
|
||||||
|
shareObjectName={"obj2"}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -100,6 +100,12 @@ div.fesl-row {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fesl-item-name {
|
||||||
|
a {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*--------------------------
|
/*--------------------------
|
||||||
Icons
|
Icons
|
||||||
----------------------------*/
|
----------------------------*/
|
||||||
|
|||||||
+2
-2
@@ -70,9 +70,9 @@ async.waterfall([
|
|||||||
commitId = stdout.replace('\n', '')
|
commitId = stdout.replace('\n', '')
|
||||||
if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
|
if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
|
||||||
assetsFileName = 'ui-assets.go';
|
assetsFileName = 'ui-assets.go';
|
||||||
var cmd = 'go-bindata-assetfs -pkg browser -nocompress=true production/...'
|
var cmd = 'go-bindata-assetfs -o bindata_assetfs.go -pkg browser -nocompress=true production/...'
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
cmd = 'go-bindata-assetfs -pkg browser -nocompress=true dev/...'
|
cmd = 'go-bindata-assetfs -o bindata_assetfs.go -pkg browser -nocompress=true dev/...'
|
||||||
}
|
}
|
||||||
console.log('Running', cmd)
|
console.log('Running', cmd)
|
||||||
exec(cmd, cb)
|
exec(cmd, cb)
|
||||||
|
|||||||
+24
-24
File diff suppressed because one or more lines are too long
@@ -89,11 +89,11 @@ check_minimum_version() {
|
|||||||
|
|
||||||
assert_is_supported_arch() {
|
assert_is_supported_arch() {
|
||||||
case "${ARCH}" in
|
case "${ARCH}" in
|
||||||
x86_64 | amd64 | aarch64 | arm* )
|
x86_64 | amd64 | aarch64 | ppc64le | arm* | s390x )
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, arm*]"
|
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*, s390x]"
|
||||||
exit 1
|
exit 1
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ function start_minio_erasure_sets()
|
|||||||
function start_minio_dist_erasure_sets()
|
function start_minio_dist_erasure_sets()
|
||||||
{
|
{
|
||||||
declare -a minio_pids
|
declare -a minio_pids
|
||||||
|
export MINIO_ACCESS_KEY=$ACCESS_KEY
|
||||||
|
export MINIO_SECRET_KEY=$SECRET_KEY
|
||||||
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
|
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
|
||||||
minio_pids[0]=$!
|
minio_pids[0]=$!
|
||||||
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
|
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets4" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets5" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets6" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets7" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets8" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets9" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets10" "http://127.0.0.1:9000${WORK_DIR}/dist-disk-sets11" "http://127.0.0.1:9001${WORK_DIR}/dist-disk-sets12" "http://127.0.0.1:9002${WORK_DIR}/dist-disk-sets13" "http://127.0.0.1:9003${WORK_DIR}/dist-disk-sets14" "http://127.0.0.1:9004${WORK_DIR}/dist-disk-sets15" "http://127.0.0.1:9005${WORK_DIR}/dist-disk-sets16" "http://127.0.0.1:9006${WORK_DIR}/dist-disk-sets17" "http://127.0.0.1:9007${WORK_DIR}/dist-disk-sets18" "http://127.0.0.1:9008${WORK_DIR}/dist-disk-sets19" "http://127.0.0.1:9009${WORK_DIR}/dist-disk-sets20" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
|
||||||
@@ -99,6 +101,8 @@ function start_minio_dist_erasure_sets()
|
|||||||
function start_minio_dist_erasure()
|
function start_minio_dist_erasure()
|
||||||
{
|
{
|
||||||
declare -a minio_pids
|
declare -a minio_pids
|
||||||
|
export MINIO_ACCESS_KEY=$ACCESS_KEY
|
||||||
|
export MINIO_SECRET_KEY=$SECRET_KEY
|
||||||
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
|
"${MINIO[@]}" server --address=:9000 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9000.log" 2>&1 &
|
||||||
minio_pids[0]=$!
|
minio_pids[0]=$!
|
||||||
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
|
"${MINIO[@]}" server --address=:9001 "http://127.0.0.1:9000${WORK_DIR}/dist-disk1" "http://127.0.0.1:9001${WORK_DIR}/dist-disk2" "http://127.0.0.1:9002${WORK_DIR}/dist-disk3" "http://127.0.0.1:9003${WORK_DIR}/dist-disk4" >"$WORK_DIR/dist-minio-9001.log" 2>&1 &
|
||||||
|
|||||||
+2
-2
@@ -54,7 +54,7 @@ type accessControlPolicy struct {
|
|||||||
// This operation uses the ACL
|
// This operation uses the ACL
|
||||||
// subresource to return the ACL of a specified bucket.
|
// subresource to return the ACL of a specified bucket.
|
||||||
func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "GetBucketACL")
|
ctx := newContext(r, w, "GetBucketACL")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
@@ -101,7 +101,7 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
|
|||||||
// This operation uses the ACL
|
// This operation uses the ACL
|
||||||
// subresource to return the ACL of a specified object.
|
// subresource to return the ACL of a specified object.
|
||||||
func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "GetObjectACL")
|
ctx := newContext(r, w, "GetObjectACL")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|||||||
+93
-266
@@ -21,10 +21,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -37,8 +35,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
minioConfigTmpFormat = "config-%s.json"
|
|
||||||
|
|
||||||
maxConfigJSONSize = 256 * 1024 // 256KiB
|
maxConfigJSONSize = 256 * 1024 // 256KiB
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,11 +43,10 @@ type mgmtQueryKey string
|
|||||||
|
|
||||||
// Only valid query params for mgmt admin APIs.
|
// Only valid query params for mgmt admin APIs.
|
||||||
const (
|
const (
|
||||||
mgmtBucket mgmtQueryKey = "bucket"
|
mgmtBucket mgmtQueryKey = "bucket"
|
||||||
mgmtPrefix mgmtQueryKey = "prefix"
|
mgmtPrefix mgmtQueryKey = "prefix"
|
||||||
mgmtLockOlderThan mgmtQueryKey = "older-than"
|
mgmtClientToken mgmtQueryKey = "clientToken"
|
||||||
mgmtClientToken mgmtQueryKey = "clientToken"
|
mgmtForceStart mgmtQueryKey = "forceStart"
|
||||||
mgmtForceStart mgmtQueryKey = "forceStart"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -275,133 +270,6 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
writeSuccessResponseJSON(w, jsonBytes)
|
writeSuccessResponseJSON(w, jsonBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateLockQueryParams - Validates query params for list/clear
|
|
||||||
// locks management APIs.
|
|
||||||
func validateLockQueryParams(vars url.Values) (string, string, time.Duration,
|
|
||||||
APIErrorCode) {
|
|
||||||
|
|
||||||
bucket := vars.Get(string(mgmtBucket))
|
|
||||||
prefix := vars.Get(string(mgmtPrefix))
|
|
||||||
olderThanStr := vars.Get(string(mgmtLockOlderThan))
|
|
||||||
|
|
||||||
// N B empty bucket name is invalid
|
|
||||||
if !IsValidBucketName(bucket) {
|
|
||||||
return "", "", time.Duration(0), ErrInvalidBucketName
|
|
||||||
}
|
|
||||||
// empty prefix is valid.
|
|
||||||
if !IsValidObjectPrefix(prefix) {
|
|
||||||
return "", "", time.Duration(0), ErrInvalidObjectName
|
|
||||||
}
|
|
||||||
|
|
||||||
// If older-than parameter was empty then set it to 0s to list
|
|
||||||
// all locks older than now.
|
|
||||||
if olderThanStr == "" {
|
|
||||||
olderThanStr = "0s"
|
|
||||||
}
|
|
||||||
duration, err := time.ParseDuration(olderThanStr)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return "", "", time.Duration(0), ErrInvalidDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
return bucket, prefix, duration, ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListLocksHandler - GET /minio/admin/v1/locks?bucket=mybucket&prefix=myprefix&older-than=10s
|
|
||||||
// - bucket is a mandatory query parameter
|
|
||||||
// - prefix and older-than are optional query parameters
|
|
||||||
// ---------
|
|
||||||
// Lists locks held on a given bucket, prefix and duration it was held for.
|
|
||||||
func (a adminAPIHandlers) ListLocksHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
|
|
||||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
|
||||||
if adminAPIErr != ErrNone {
|
|
||||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := r.URL.Query()
|
|
||||||
bucket, prefix, duration, adminAPIErr := validateLockQueryParams(vars)
|
|
||||||
if adminAPIErr != ErrNone {
|
|
||||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch lock information of locks matching bucket/prefix that
|
|
||||||
// are available for longer than duration.
|
|
||||||
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix,
|
|
||||||
duration)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal list of locks as json.
|
|
||||||
jsonBytes, err := json.Marshal(volLocks)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reply with list of locks held on bucket, matching prefix
|
|
||||||
// held longer than duration supplied, as json.
|
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearLocksHandler - DELETE /minio/admin/v1/locks?bucket=mybucket&prefix=myprefix&duration=duration
|
|
||||||
// - bucket is a mandatory query parameter
|
|
||||||
// - prefix and older-than are optional query parameters
|
|
||||||
// ---------
|
|
||||||
// Clear locks held on a given bucket, prefix and duration it was held for.
|
|
||||||
func (a adminAPIHandlers) ClearLocksHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, "ClearLocks")
|
|
||||||
|
|
||||||
// Get object layer instance.
|
|
||||||
objLayer := newObjectLayerFn()
|
|
||||||
if objLayer == nil {
|
|
||||||
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
|
||||||
if adminAPIErr != ErrNone {
|
|
||||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := r.URL.Query()
|
|
||||||
bucket, prefix, duration, adminAPIErr := validateLockQueryParams(vars)
|
|
||||||
if adminAPIErr != ErrNone {
|
|
||||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch lock information of locks matching bucket/prefix that
|
|
||||||
// are held for longer than duration.
|
|
||||||
volLocks, err := listPeerLocksInfo(globalAdminPeers, bucket, prefix,
|
|
||||||
duration)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal list of locks as json.
|
|
||||||
jsonBytes, err := json.Marshal(volLocks)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
objLayer.ClearLocks(ctx, volLocks)
|
|
||||||
|
|
||||||
// Reply with list of locks cleared, as json.
|
|
||||||
writeSuccessResponseJSON(w, jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractHealInitParams - Validates params for heal init API.
|
// extractHealInitParams - Validates params for heal init API.
|
||||||
func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
|
func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
|
||||||
hs madmin.HealOpts, clientToken string, forceStart bool,
|
hs madmin.HealOpts, clientToken string, forceStart bool,
|
||||||
@@ -464,7 +332,7 @@ func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
|
|||||||
// sequence. However, if the force-start flag is provided, the server
|
// sequence. However, if the force-start flag is provided, the server
|
||||||
// aborts the running heal sequence and starts a new one.
|
// aborts the running heal sequence and starts a new one.
|
||||||
func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "Heal")
|
ctx := newContext(r, w, "Heal")
|
||||||
|
|
||||||
// Get object layer instance.
|
// Get object layer instance.
|
||||||
objLayer := newObjectLayerFn()
|
objLayer := newObjectLayerFn()
|
||||||
@@ -570,6 +438,15 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// GetConfigHandler - GET /minio/admin/v1/config
|
// GetConfigHandler - GET /minio/admin/v1/config
|
||||||
// Get config.json of this minio setup.
|
// Get config.json of this minio setup.
|
||||||
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "GetConfigHandler")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Validate request signature.
|
// Validate request signature.
|
||||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
if adminAPIErr != ErrNone {
|
if adminAPIErr != ErrNone {
|
||||||
@@ -577,26 +454,28 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take a read lock on minio/config.json. NB minio is a
|
config, err := readServerConfig(ctx, objectAPI)
|
||||||
// reserved bucket name and wouldn't conflict with normal
|
|
||||||
// object operations.
|
|
||||||
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
|
||||||
if configLock.GetRLock(globalObjectTimeout) != nil {
|
|
||||||
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer configLock.RUnlock()
|
|
||||||
|
|
||||||
// Get config.json - in distributed mode, the configuration
|
|
||||||
// occurring on a quorum of the servers is returned.
|
|
||||||
configBytes, err := getPeerConfig(globalAdminPeers)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessResponseJSON(w, configBytes)
|
configData, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password := config.GetCredential().SecretKey
|
||||||
|
econfigData, err := madmin.EncryptServerConfigData(password, configData)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, econfigData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// toAdminAPIErrCode - converts errXLWriteQuorum error to admin API
|
// toAdminAPIErrCode - converts errXLWriteQuorum error to admin API
|
||||||
@@ -605,65 +484,15 @@ func toAdminAPIErrCode(err error) APIErrorCode {
|
|||||||
switch err {
|
switch err {
|
||||||
case errXLWriteQuorum:
|
case errXLWriteQuorum:
|
||||||
return ErrAdminConfigNoQuorum
|
return ErrAdminConfigNoQuorum
|
||||||
|
default:
|
||||||
|
return toAPIErrorCode(err)
|
||||||
}
|
}
|
||||||
return toAPIErrorCode(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetConfigResult - represents detailed results of a set-config
|
|
||||||
// operation.
|
|
||||||
type nodeSummary struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
ErrSet bool `json:"errSet"`
|
|
||||||
ErrMsg string `json:"errMsg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type setConfigResult struct {
|
|
||||||
NodeResults []nodeSummary `json:"nodeResults"`
|
|
||||||
Status bool `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeSetConfigResponse - writes setConfigResult value as json
|
|
||||||
// depending on the status.
|
|
||||||
func writeSetConfigResponse(w http.ResponseWriter, peers adminPeers,
|
|
||||||
errs []error, status bool, reqURL *url.URL) {
|
|
||||||
|
|
||||||
var nodeResults []nodeSummary
|
|
||||||
// Build nodeResults based on error values received during
|
|
||||||
// set-config operation.
|
|
||||||
for i := range errs {
|
|
||||||
nodeResults = append(nodeResults, nodeSummary{
|
|
||||||
Name: peers[i].addr,
|
|
||||||
ErrSet: errs[i] != nil,
|
|
||||||
ErrMsg: fmt.Sprintf("%v", errs[i]),
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
result := setConfigResult{
|
|
||||||
Status: status,
|
|
||||||
NodeResults: nodeResults,
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following elaborate json encoding is to avoid escaping
|
|
||||||
// '<', '>' in <nil>. Note: json.Encoder.Encode() adds a
|
|
||||||
// gratuitous "\n".
|
|
||||||
var resultBuf bytes.Buffer
|
|
||||||
enc := json.NewEncoder(&resultBuf)
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
jsonErr := enc.Encode(result)
|
|
||||||
if jsonErr != nil {
|
|
||||||
writeErrorResponseJSON(w, toAPIErrorCode(jsonErr), reqURL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
writeSuccessResponseJSON(w, resultBuf.Bytes())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfigHandler - PUT /minio/admin/v1/config
|
// SetConfigHandler - PUT /minio/admin/v1/config
|
||||||
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SetConfigHandler")
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
// Get current object layer instance.
|
// Get current object layer instance.
|
||||||
objectAPI := newObjectLayerFn()
|
objectAPI := newObjectLayerFn()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -671,12 +500,6 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deny if WORM is enabled
|
|
||||||
if globalWORMEnabled {
|
|
||||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate request signature.
|
// Validate request signature.
|
||||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
if adminAPIErr != ErrNone {
|
if adminAPIErr != ErrNone {
|
||||||
@@ -698,7 +521,13 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configBytes := configBuf[:n]
|
password := globalServerConfig.GetCredential().SecretKey
|
||||||
|
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Validate JSON provided in the request body: check the
|
// Validate JSON provided in the request body: check the
|
||||||
// client has not sent JSON objects with duplicate keys.
|
// client has not sent JSON objects with duplicate keys.
|
||||||
@@ -712,7 +541,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
err = json.Unmarshal(configBytes, &config)
|
err = json.Unmarshal(configBytes, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,53 +556,45 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write config received from request onto a temporary file on
|
if err = config.Validate(); err != nil {
|
||||||
// all nodes.
|
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||||
tmpFileName := fmt.Sprintf(minioConfigTmpFormat, mustGetUUID())
|
|
||||||
errs := writeTmpConfigPeers(globalAdminPeers, tmpFileName, configBytes)
|
|
||||||
|
|
||||||
// Check if the operation succeeded in quorum or more nodes.
|
|
||||||
rErr := reduceWriteQuorumErrs(ctx, errs, nil, len(globalAdminPeers)/2+1)
|
|
||||||
if rErr != nil {
|
|
||||||
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take a lock on minio/config.json. NB minio is a reserved
|
if err = saveServerConfig(objectAPI, &config); err != nil {
|
||||||
// bucket name and wouldn't conflict with normal object
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
// operations.
|
|
||||||
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
|
||||||
if configLock.GetLock(globalObjectTimeout) != nil {
|
|
||||||
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer configLock.Unlock()
|
|
||||||
|
|
||||||
// Rename the temporary config file to config.json
|
|
||||||
errs = commitConfigPeers(globalAdminPeers, tmpFileName)
|
|
||||||
rErr = reduceWriteQuorumErrs(ctx, errs, nil, len(globalAdminPeers)/2+1)
|
|
||||||
if rErr != nil {
|
|
||||||
writeSetConfigResponse(w, globalAdminPeers, errs, false, r.URL)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverMux (cmd/server-mux.go) implements graceful shutdown,
|
// Reply to the client before restarting minio server.
|
||||||
// where all listeners are closed and process restart/shutdown
|
writeSuccessResponseHeadersOnly(w)
|
||||||
// happens after 5s or completion of all ongoing http
|
|
||||||
// requests, whichever is earlier.
|
|
||||||
writeSetConfigResponse(w, globalAdminPeers, errs, true, r.URL)
|
|
||||||
|
|
||||||
// Restart all node for the modified config to take effect.
|
|
||||||
sendServiceCmd(globalAdminPeers, serviceRestart)
|
sendServiceCmd(globalAdminPeers, serviceRestart)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigCredsHandler - POST /minio/admin/v1/config/credential
|
// UpdateCredsHandler - POST /minio/admin/v1/config/credential
|
||||||
// ----------
|
// ----------
|
||||||
// Update credentials in a minio server. In a distributed setup,
|
// Update credentials in a minio server. In a distributed setup,
|
||||||
// update all the servers in the cluster.
|
// update all the servers in the cluster.
|
||||||
func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
||||||
r *http.Request) {
|
r *http.Request) {
|
||||||
|
|
||||||
|
ctx := newContext(r, w, "UpdateCredentialsHandler")
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(w, ErrServerNotInitialized, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid setting new credentials when they are already passed
|
||||||
|
// by the environment. Deny if WORM is enabled.
|
||||||
|
if globalIsEnvCreds {
|
||||||
|
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate request
|
// Authenticate request
|
||||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||||
if adminAPIErr != ErrNone {
|
if adminAPIErr != ErrNone {
|
||||||
@@ -781,18 +602,32 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid setting new credentials when they are already passed
|
// Read configuration bytes from request body.
|
||||||
// by the environment. Deny if WORM is enabled.
|
configBuf := make([]byte, maxConfigJSONSize+1)
|
||||||
if globalIsEnvCreds || globalWORMEnabled {
|
n, err := io.ReadFull(r.Body, configBuf)
|
||||||
writeErrorResponseJSON(w, ErrMethodNotAllowed, r.URL)
|
if err == nil {
|
||||||
|
// More than maxConfigSize bytes were available
|
||||||
|
writeErrorResponseJSON(w, ErrAdminConfigTooLarge, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != io.ErrUnexpectedEOF {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password := globalServerConfig.GetCredential().SecretKey
|
||||||
|
configBytes, err := madmin.DecryptServerConfigData(password, bytes.NewReader(configBuf[:n]))
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(w, ErrAdminConfigBadJSON, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode request body
|
// Decode request body
|
||||||
var req madmin.SetCredsReq
|
var req madmin.SetCredsReq
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
if err = json.Unmarshal(configBytes, &req); err != nil {
|
||||||
if err != nil {
|
logger.LogIf(ctx, err)
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL)
|
writeErrorResponseJSON(w, ErrRequestBodyParse, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -803,33 +638,25 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take a lock on minio/config.json. Prevents concurrent
|
|
||||||
// config file updates.
|
|
||||||
configLock := globalNSMutex.NewNSLock(minioReservedBucket, minioConfigFile)
|
|
||||||
if configLock.GetLock(globalObjectTimeout) != nil {
|
|
||||||
writeErrorResponseJSON(w, ErrOperationTimedOut, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer configLock.Unlock()
|
|
||||||
|
|
||||||
// Acquire lock before updating global configuration.
|
// Acquire lock before updating global configuration.
|
||||||
globalServerConfigMu.Lock()
|
globalServerConfigMu.Lock()
|
||||||
defer globalServerConfigMu.Unlock()
|
defer globalServerConfigMu.Unlock()
|
||||||
|
|
||||||
// Update local credentials in memory.
|
// Update local credentials in memory.
|
||||||
globalServerConfig.SetCredential(creds)
|
globalServerConfig.SetCredential(creds)
|
||||||
if err = globalServerConfig.Save(getConfigFile()); err != nil {
|
|
||||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
if err = saveServerConfig(objectAPI, globalServerConfig); err != nil {
|
||||||
|
writeErrorResponseJSON(w, toAdminAPIErrCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify all other Minio peers to update credentials
|
// Notify all other Minio peers to update credentials
|
||||||
for host, err := range globalNotificationSys.SetCredentials(creds) {
|
for host, err := range globalNotificationSys.LoadCredentials() {
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", host.String())
|
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", host.String())
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
ctx := logger.SetReqInfo(ctx, reqInfo)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this stage, the operation is successful, return 200 OK
|
// Reply to the client before restarting minio server.
|
||||||
w.WriteHeader(http.StatusOK)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
|
|||||||
+115
-321
@@ -26,7 +26,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -38,21 +37,39 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
configJSON = []byte(`{
|
configJSON = []byte(`{
|
||||||
"version": "13",
|
"version": "28",
|
||||||
"credential": {
|
"credential": {
|
||||||
"accessKey": "minio",
|
"accessKey": "minio",
|
||||||
"secretKey": "minio123"
|
"secretKey": "minio123"
|
||||||
},
|
},
|
||||||
"region": "us-west-1",
|
"region": "",
|
||||||
"logger": {
|
"browser": "on",
|
||||||
"console": {
|
"worm": "off",
|
||||||
"enable": true,
|
"domain": "",
|
||||||
"level": "fatal"
|
"storageclass": {
|
||||||
},
|
"standard": "",
|
||||||
"file": {
|
"rrs": ""
|
||||||
"enable": false,
|
},
|
||||||
"fileName": "",
|
"cache": {
|
||||||
"level": ""
|
"drives": [],
|
||||||
|
"expiry": 90,
|
||||||
|
"maxuse": 80,
|
||||||
|
"exclude": []
|
||||||
|
},
|
||||||
|
"kms": {
|
||||||
|
"vault": {
|
||||||
|
"endpoint": "",
|
||||||
|
"auth": {
|
||||||
|
"type": "",
|
||||||
|
"approle": {
|
||||||
|
"id": "",
|
||||||
|
"secret": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"key-id": {
|
||||||
|
"name": "",
|
||||||
|
"version": 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notify": {
|
"notify": {
|
||||||
@@ -63,6 +80,7 @@ var (
|
|||||||
"exchange": "",
|
"exchange": "",
|
||||||
"routingKey": "",
|
"routingKey": "",
|
||||||
"exchangeType": "",
|
"exchangeType": "",
|
||||||
|
"deliveryMode": 0,
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"immediate": false,
|
"immediate": false,
|
||||||
"durable": false,
|
"durable": false,
|
||||||
@@ -71,6 +89,47 @@ var (
|
|||||||
"autoDeleted": false
|
"autoDeleted": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"elasticsearch": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"format": "",
|
||||||
|
"url": "",
|
||||||
|
"index": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kafka": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"brokers": null,
|
||||||
|
"topic": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"broker": "",
|
||||||
|
"topic": "",
|
||||||
|
"qos": 0,
|
||||||
|
"clientId": "",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"reconnectInterval": 0,
|
||||||
|
"keepAliveInterval": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mysql": {
|
||||||
|
"1": {
|
||||||
|
"enable": false,
|
||||||
|
"format": "",
|
||||||
|
"dsnString": "",
|
||||||
|
"table": "",
|
||||||
|
"host": "",
|
||||||
|
"port": "",
|
||||||
|
"user": "",
|
||||||
|
"password": "",
|
||||||
|
"database": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"nats": {
|
"nats": {
|
||||||
"1": {
|
"1": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
@@ -90,24 +149,10 @@ var (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"elasticsearch": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"url": "",
|
|
||||||
"index": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"redis": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"address": "",
|
|
||||||
"password": "",
|
|
||||||
"key": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"postgresql": {
|
"postgresql": {
|
||||||
"1": {
|
"1": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
|
"format": "",
|
||||||
"connectionString": "",
|
"connectionString": "",
|
||||||
"table": "",
|
"table": "",
|
||||||
"host": "",
|
"host": "",
|
||||||
@@ -117,11 +162,13 @@ var (
|
|||||||
"database": ""
|
"database": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"kafka": {
|
"redis": {
|
||||||
"1": {
|
"1": {
|
||||||
"enable": false,
|
"enable": false,
|
||||||
"brokers": null,
|
"format": "",
|
||||||
"topic": ""
|
"address": "",
|
||||||
|
"password": "",
|
||||||
|
"key": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"webhook": {
|
"webhook": {
|
||||||
@@ -130,8 +177,20 @@ var (
|
|||||||
"endpoint": ""
|
"endpoint": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}`)
|
"logger": {
|
||||||
|
"console": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"1": {
|
||||||
|
"enabled": false,
|
||||||
|
"endpoint": "http://user:example@localhost:9001/api/endpoint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// adminXLTestBed - encapsulates subsystems that need to be setup for
|
// adminXLTestBed - encapsulates subsystems that need to be setup for
|
||||||
@@ -149,17 +208,17 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
|||||||
// reset global variables to start afresh.
|
// reset global variables to start afresh.
|
||||||
resetTestGlobals()
|
resetTestGlobals()
|
||||||
|
|
||||||
// Initialize minio server config.
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Initializing objectLayer for HealFormatHandler.
|
// Initializing objectLayer for HealFormatHandler.
|
||||||
objLayer, xlDirs, xlErr := initTestXLObjLayer()
|
objLayer, xlDirs, xlErr := initTestXLObjLayer()
|
||||||
if xlErr != nil {
|
if xlErr != nil {
|
||||||
return nil, xlErr
|
return nil, xlErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize minio server config.
|
||||||
|
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize boot time
|
// Initialize boot time
|
||||||
globalBootTime = UTCNow()
|
globalBootTime = UTCNow()
|
||||||
|
|
||||||
@@ -176,10 +235,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
|||||||
// Init global heal state
|
// Init global heal state
|
||||||
initAllHealState(globalIsXL)
|
initAllHealState(globalIsXL)
|
||||||
|
|
||||||
globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints)
|
globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new policy system.
|
// Create new policy system.
|
||||||
globalPolicySys = NewPolicySys()
|
globalPolicySys = NewPolicySys()
|
||||||
@@ -189,17 +245,15 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
|||||||
registerAdminRouter(adminRouter)
|
registerAdminRouter(adminRouter)
|
||||||
|
|
||||||
return &adminXLTestBed{
|
return &adminXLTestBed{
|
||||||
configPath: rootPath,
|
xlDirs: xlDirs,
|
||||||
xlDirs: xlDirs,
|
objLayer: objLayer,
|
||||||
objLayer: objLayer,
|
router: adminRouter,
|
||||||
router: adminRouter,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TearDown - method that resets the test bed for subsequent unit
|
// TearDown - method that resets the test bed for subsequent unit
|
||||||
// tests to start afresh.
|
// tests to start afresh.
|
||||||
func (atb *adminXLTestBed) TearDown() {
|
func (atb *adminXLTestBed) TearDown() {
|
||||||
os.RemoveAll(atb.configPath)
|
|
||||||
removeRoots(atb.xlDirs)
|
removeRoots(atb.xlDirs)
|
||||||
resetTestGlobals()
|
resetTestGlobals()
|
||||||
}
|
}
|
||||||
@@ -538,8 +592,14 @@ func TestServiceSetCreds(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("JSONify err: %v", err)
|
t.Fatalf("JSONify err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ebody, err := madmin.EncryptServerConfigData(credentials.SecretKey, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Construct setCreds request
|
// Construct setCreds request
|
||||||
req, err := getServiceCmdRequest(setCreds, credentials, body)
|
req, err := getServiceCmdRequest(setCreds, credentials, ebody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to build service status request %v", err)
|
t.Fatalf("Failed to build service status request %v", err)
|
||||||
}
|
}
|
||||||
@@ -570,193 +630,6 @@ func TestServiceSetCreds(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mkLockQueryVal - helper function to build lock query param.
|
|
||||||
func mkLockQueryVal(bucket, prefix, durationStr string) url.Values {
|
|
||||||
qVal := url.Values{}
|
|
||||||
qVal.Set(string(mgmtBucket), bucket)
|
|
||||||
qVal.Set(string(mgmtPrefix), prefix)
|
|
||||||
qVal.Set(string(mgmtLockOlderThan), durationStr)
|
|
||||||
return qVal
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for locks list management REST API.
|
|
||||||
func TestListLocksHandler(t *testing.T) {
|
|
||||||
adminTestBed, err := prepareAdminXLTestBed()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
|
||||||
}
|
|
||||||
defer adminTestBed.TearDown()
|
|
||||||
|
|
||||||
// Initialize admin peers to make admin RPC calls.
|
|
||||||
globalMinioAddr = "127.0.0.1:9000"
|
|
||||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
bucket string
|
|
||||||
prefix string
|
|
||||||
duration string
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
// Test 1 - valid testcase
|
|
||||||
{
|
|
||||||
bucket: "mybucket",
|
|
||||||
prefix: "myobject",
|
|
||||||
duration: "1s",
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
// Test 2 - invalid duration
|
|
||||||
{
|
|
||||||
bucket: "mybucket",
|
|
||||||
prefix: "myprefix",
|
|
||||||
duration: "invalidDuration",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
// Test 3 - invalid bucket name
|
|
||||||
{
|
|
||||||
bucket: `invalid\\Bucket`,
|
|
||||||
prefix: "myprefix",
|
|
||||||
duration: "1h",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
// Test 4 - invalid prefix
|
|
||||||
{
|
|
||||||
bucket: "mybucket",
|
|
||||||
prefix: `invalid\\Prefix`,
|
|
||||||
duration: "1h",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range testCases {
|
|
||||||
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.duration)
|
|
||||||
req, err := newTestRequest("GET", "/minio/admin/v1/locks?"+queryVal.Encode(), 0, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d - Failed to construct list locks request - %v", i+1, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cred := globalServerConfig.GetCredential()
|
|
||||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d - Failed to sign list locks request - %v", i+1, err)
|
|
||||||
}
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
adminTestBed.router.ServeHTTP(rec, req)
|
|
||||||
if test.expectedStatus != rec.Code {
|
|
||||||
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.expectedStatus, rec.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for locks clear management REST API.
|
|
||||||
func TestClearLocksHandler(t *testing.T) {
|
|
||||||
adminTestBed, err := prepareAdminXLTestBed()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
|
||||||
}
|
|
||||||
defer adminTestBed.TearDown()
|
|
||||||
|
|
||||||
// Initialize admin peers to make admin RPC calls.
|
|
||||||
initGlobalAdminPeers(mustGetNewEndpointList("http://127.0.0.1:9000/d1"))
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
bucket string
|
|
||||||
prefix string
|
|
||||||
duration string
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
// Test 1 - valid testcase
|
|
||||||
{
|
|
||||||
bucket: "mybucket",
|
|
||||||
prefix: "myobject",
|
|
||||||
duration: "1s",
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
// Test 2 - invalid duration
|
|
||||||
{
|
|
||||||
bucket: "mybucket",
|
|
||||||
prefix: "myprefix",
|
|
||||||
duration: "invalidDuration",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
// Test 3 - invalid bucket name
|
|
||||||
{
|
|
||||||
bucket: `invalid\\Bucket`,
|
|
||||||
prefix: "myprefix",
|
|
||||||
duration: "1h",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
// Test 4 - invalid prefix
|
|
||||||
{
|
|
||||||
bucket: "mybucket",
|
|
||||||
prefix: `invalid\\Prefix`,
|
|
||||||
duration: "1h",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range testCases {
|
|
||||||
queryVal := mkLockQueryVal(test.bucket, test.prefix, test.duration)
|
|
||||||
req, err := newTestRequest("DELETE", "/minio/admin/v1/locks?"+queryVal.Encode(), 0, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d - Failed to construct clear locks request - %v", i+1, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cred := globalServerConfig.GetCredential()
|
|
||||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d - Failed to sign clear locks request - %v", i+1, err)
|
|
||||||
}
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
adminTestBed.router.ServeHTTP(rec, req)
|
|
||||||
if test.expectedStatus != rec.Code {
|
|
||||||
t.Errorf("Test %d - Expected HTTP status code %d but received %d", i+1, test.expectedStatus, rec.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test for lock query param validation helper function.
|
|
||||||
func TestValidateLockQueryParams(t *testing.T) {
|
|
||||||
// reset globals.
|
|
||||||
// this is to make sure that the tests are not affected by modified globals.
|
|
||||||
resetTestGlobals()
|
|
||||||
// initialize NSLock.
|
|
||||||
initNSLock(false)
|
|
||||||
// Sample query values for test cases.
|
|
||||||
allValidVal := mkLockQueryVal("bucket", "prefix", "1s")
|
|
||||||
invalidBucketVal := mkLockQueryVal(`invalid\\Bucket`, "prefix", "1s")
|
|
||||||
invalidPrefixVal := mkLockQueryVal("bucket", `invalid\\Prefix`, "1s")
|
|
||||||
invalidOlderThanVal := mkLockQueryVal("bucket", "prefix", "invalidDuration")
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
qVals url.Values
|
|
||||||
apiErr APIErrorCode
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
qVals: invalidBucketVal,
|
|
||||||
apiErr: ErrInvalidBucketName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
qVals: invalidPrefixVal,
|
|
||||||
apiErr: ErrInvalidObjectName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
qVals: invalidOlderThanVal,
|
|
||||||
apiErr: ErrInvalidDuration,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
qVals: allValidVal,
|
|
||||||
apiErr: ErrNone,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range testCases {
|
|
||||||
_, _, _, apiErr := validateLockQueryParams(test.qVals)
|
|
||||||
if apiErr != test.apiErr {
|
|
||||||
t.Errorf("Test %d - Expected error %v but received %v", i+1, test.apiErr, apiErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildAdminRequest - helper function to build an admin API request.
|
// buildAdminRequest - helper function to build an admin API request.
|
||||||
func buildAdminRequest(queryVal url.Values, method, path string,
|
func buildAdminRequest(queryVal url.Values, method, path string,
|
||||||
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) {
|
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) {
|
||||||
@@ -826,8 +699,14 @@ func TestSetConfigHandler(t *testing.T) {
|
|||||||
queryVal := url.Values{}
|
queryVal := url.Values{}
|
||||||
queryVal.Set("config", "")
|
queryVal.Set("config", "")
|
||||||
|
|
||||||
|
password := globalServerConfig.GetCredential().SecretKey
|
||||||
|
econfigJSON, err := madmin.EncryptServerConfigData(password, configJSON)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
||||||
int64(len(configJSON)), bytes.NewReader(configJSON))
|
int64(len(econfigJSON)), bytes.NewReader(econfigJSON))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to construct set-config object request - %v", err)
|
t.Fatalf("Failed to construct set-config object request - %v", err)
|
||||||
}
|
}
|
||||||
@@ -838,16 +717,6 @@ func TestSetConfigHandler(t *testing.T) {
|
|||||||
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := setConfigResult{}
|
|
||||||
err = json.NewDecoder(rec.Body).Decode(&result)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to decode set config result json %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !result.Status {
|
|
||||||
t.Error("Expected set-config to succeed, but failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that a very large config file returns an error.
|
// Check that a very large config file returns an error.
|
||||||
{
|
{
|
||||||
// Make a large enough config string
|
// Make a large enough config string
|
||||||
@@ -870,7 +739,7 @@ func TestSetConfigHandler(t *testing.T) {
|
|||||||
// Check that a config with duplicate keys in an object return
|
// Check that a config with duplicate keys in an object return
|
||||||
// error.
|
// error.
|
||||||
{
|
{
|
||||||
invalidCfg := append(configJSON[:len(configJSON)-1], []byte(`, "version": "15"}`)...)
|
invalidCfg := append(econfigJSON[:len(econfigJSON)-1], []byte(`, "version": "15"}`)...)
|
||||||
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
||||||
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -968,81 +837,6 @@ func TestToAdminAPIErr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteSetConfigResponse(t *testing.T) {
|
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
testCases := []struct {
|
|
||||||
status bool
|
|
||||||
errs []error
|
|
||||||
}{
|
|
||||||
// 1. all nodes returned success.
|
|
||||||
{
|
|
||||||
status: true,
|
|
||||||
errs: []error{nil, nil, nil, nil},
|
|
||||||
},
|
|
||||||
// 2. some nodes returned errors.
|
|
||||||
{
|
|
||||||
status: false,
|
|
||||||
errs: []error{errDiskNotFound, nil, errDiskAccessDenied, errFaultyDisk},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
testPeers := []adminPeer{
|
|
||||||
{
|
|
||||||
addr: "localhost:9001",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
addr: "localhost:9002",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
addr: "localhost:9003",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
addr: "localhost:9004",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
testURL, err := url.Parse("http://dummy.com")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to parse a place-holder url")
|
|
||||||
}
|
|
||||||
|
|
||||||
var actualResult setConfigResult
|
|
||||||
for i, test := range testCases {
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
writeSetConfigResponse(rec, testPeers, test.errs, test.status, testURL)
|
|
||||||
resp := rec.Result()
|
|
||||||
jsonBytes, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: Failed to read response %v", i+1, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(jsonBytes, &actualResult)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: Failed to unmarshal json %v", i+1, err)
|
|
||||||
}
|
|
||||||
if actualResult.Status != test.status {
|
|
||||||
t.Errorf("Test %d: Expected status %v but received %v", i+1, test.status, actualResult.Status)
|
|
||||||
}
|
|
||||||
for p, res := range actualResult.NodeResults {
|
|
||||||
if res.Name != testPeers[p].addr {
|
|
||||||
t.Errorf("Test %d: Expected node name %s but received %s", i+1, testPeers[p].addr, res.Name)
|
|
||||||
}
|
|
||||||
expectedErrMsg := fmt.Sprintf("%v", test.errs[p])
|
|
||||||
if res.ErrMsg != expectedErrMsg {
|
|
||||||
t.Errorf("Test %d: Expected error %s but received %s", i+1, expectedErrMsg, res.ErrMsg)
|
|
||||||
}
|
|
||||||
expectedErrSet := test.errs[p] != nil
|
|
||||||
if res.ErrSet != expectedErrSet {
|
|
||||||
t.Errorf("Test %d: Expected ErrSet %v but received %v", i+1, expectedErrSet, res.ErrSet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mkHealStartReq(t *testing.T, bucket, prefix string,
|
func mkHealStartReq(t *testing.T, bucket, prefix string,
|
||||||
opts madmin.HealOpts) *http.Request {
|
opts madmin.HealOpts) *http.Request {
|
||||||
|
|
||||||
|
|||||||
+3
-10
@@ -53,13 +53,6 @@ func registerAdminRouter(router *mux.Router) {
|
|||||||
// Info operations
|
// Info operations
|
||||||
adminV1Router.Methods(http.MethodGet).Path("/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
adminV1Router.Methods(http.MethodGet).Path("/info").HandlerFunc(httpTraceAll(adminAPI.ServerInfoHandler))
|
||||||
|
|
||||||
/// Lock operations
|
|
||||||
|
|
||||||
// List Locks
|
|
||||||
adminV1Router.Methods(http.MethodGet).Path("/locks").HandlerFunc(httpTraceAll(adminAPI.ListLocksHandler))
|
|
||||||
// Clear locks
|
|
||||||
adminV1Router.Methods(http.MethodDelete).Path("/locks").HandlerFunc(httpTraceAll(adminAPI.ClearLocksHandler))
|
|
||||||
|
|
||||||
/// Heal operations
|
/// Heal operations
|
||||||
|
|
||||||
// Heal processing endpoint.
|
// Heal processing endpoint.
|
||||||
@@ -70,9 +63,9 @@ func registerAdminRouter(router *mux.Router) {
|
|||||||
/// Config operations
|
/// Config operations
|
||||||
|
|
||||||
// Update credentials
|
// Update credentials
|
||||||
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceAll(adminAPI.UpdateCredentialsHandler))
|
adminV1Router.Methods(http.MethodPut).Path("/config/credential").HandlerFunc(httpTraceHdrs(adminAPI.UpdateCredentialsHandler))
|
||||||
// Get config
|
// Get config
|
||||||
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceAll(adminAPI.GetConfigHandler))
|
adminV1Router.Methods(http.MethodGet).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.GetConfigHandler))
|
||||||
// Set config
|
// Set config
|
||||||
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceAll(adminAPI.SetConfigHandler))
|
adminV1Router.Methods(http.MethodPut).Path("/config").HandlerFunc(httpTraceHdrs(adminAPI.SetConfigHandler))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -54,19 +53,6 @@ func (rpcClient *AdminRPCClient) ReInitFormat(dryRun bool) error {
|
|||||||
return rpcClient.Call(adminServiceName+".ReInitFormat", &args, &reply)
|
return rpcClient.Call(adminServiceName+".ReInitFormat", &args, &reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListLocks - Sends list locks command to remote server via RPC.
|
|
||||||
func (rpcClient *AdminRPCClient) ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
|
||||||
args := ListLocksQuery{
|
|
||||||
Bucket: bucket,
|
|
||||||
Prefix: prefix,
|
|
||||||
Duration: duration,
|
|
||||||
}
|
|
||||||
var reply []VolumeLockInfo
|
|
||||||
|
|
||||||
err := rpcClient.Call(adminServiceName+".ListLocks", &args, &reply)
|
|
||||||
return reply, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
||||||
func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
|
func (rpcClient *AdminRPCClient) ServerInfo() (sid ServerInfoData, err error) {
|
||||||
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
|
err = rpcClient.Call(adminServiceName+".ServerInfo", &AuthArgs{}, &sid)
|
||||||
@@ -82,29 +68,6 @@ func (rpcClient *AdminRPCClient) GetConfig() ([]byte, error) {
|
|||||||
return reply, err
|
return reply, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteTmpConfig - writes config file content to a temporary file on a remote node.
|
|
||||||
func (rpcClient *AdminRPCClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
|
|
||||||
args := WriteConfigArgs{
|
|
||||||
TmpFileName: tmpFileName,
|
|
||||||
Buf: configBytes,
|
|
||||||
}
|
|
||||||
reply := VoidReply{}
|
|
||||||
|
|
||||||
err := rpcClient.Call(adminServiceName+".WriteTmpConfig", &args, &reply)
|
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitConfig - Move the new config in tmpFileName onto config.json on a remote node.
|
|
||||||
func (rpcClient *AdminRPCClient) CommitConfig(tmpFileName string) error {
|
|
||||||
args := CommitConfigArgs{FileName: tmpFileName}
|
|
||||||
reply := VoidReply{}
|
|
||||||
|
|
||||||
err := rpcClient.Call(adminServiceName+".CommitConfig", &args, &reply)
|
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAdminRPCClient - returns new admin RPC client.
|
// NewAdminRPCClient - returns new admin RPC client.
|
||||||
func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
||||||
scheme := "http"
|
scheme := "http"
|
||||||
@@ -147,11 +110,8 @@ func NewAdminRPCClient(host *xnet.Host) (*AdminRPCClient, error) {
|
|||||||
type adminCmdRunner interface {
|
type adminCmdRunner interface {
|
||||||
SignalService(s serviceSignal) error
|
SignalService(s serviceSignal) error
|
||||||
ReInitFormat(dryRun bool) error
|
ReInitFormat(dryRun bool) error
|
||||||
ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
|
|
||||||
ServerInfo() (ServerInfoData, error)
|
ServerInfo() (ServerInfoData, error)
|
||||||
GetConfig() ([]byte, error)
|
GetConfig() ([]byte, error)
|
||||||
WriteTmpConfig(tmpFileName string, configBytes []byte) error
|
|
||||||
CommitConfig(tmpFileName string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// adminPeer - represents an entity that implements admin API RPCs.
|
// adminPeer - represents an entity that implements admin API RPCs.
|
||||||
@@ -244,56 +204,6 @@ func sendServiceCmd(cps adminPeers, cmd serviceSignal) {
|
|||||||
errs[0] = invokeServiceCmd(cps[0], cmd)
|
errs[0] = invokeServiceCmd(cps[0], cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// listPeerLocksInfo - fetch list of locks held on the given bucket,
|
|
||||||
// matching prefix held longer than duration from all peer servers.
|
|
||||||
func listPeerLocksInfo(peers adminPeers, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
|
||||||
// Used to aggregate volume lock information from all nodes.
|
|
||||||
allLocks := make([][]VolumeLockInfo, len(peers))
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
localPeer := peers[0]
|
|
||||||
remotePeers := peers[1:]
|
|
||||||
for i, remotePeer := range remotePeers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, remotePeer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
// `remotePeers` is right-shifted by one position relative to `peers`
|
|
||||||
allLocks[idx], errs[idx] = remotePeer.cmdRunner.ListLocks(bucket, prefix, duration)
|
|
||||||
}(i+1, remotePeer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
allLocks[0], errs[0] = localPeer.cmdRunner.ListLocks(bucket, prefix, duration)
|
|
||||||
|
|
||||||
// Summarizing errors received for ListLocks RPC across all
|
|
||||||
// nodes. N B the possible unavailability of quorum in errors
|
|
||||||
// applies only to distributed setup.
|
|
||||||
errCount, err := reduceErrs(errs, []error{})
|
|
||||||
if err != nil {
|
|
||||||
if errCount >= (len(peers)/2 + 1) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, InsufficientReadQuorum{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group lock information across nodes by (bucket, object)
|
|
||||||
// pair. For readability only.
|
|
||||||
paramLockMap := make(map[nsParam][]VolumeLockInfo)
|
|
||||||
for _, nodeLocks := range allLocks {
|
|
||||||
for _, lockInfo := range nodeLocks {
|
|
||||||
param := nsParam{
|
|
||||||
volume: lockInfo.Bucket,
|
|
||||||
path: lockInfo.Object,
|
|
||||||
}
|
|
||||||
paramLockMap[param] = append(paramLockMap[param], lockInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
groupedLockInfos := []VolumeLockInfo{}
|
|
||||||
for _, volLocks := range paramLockMap {
|
|
||||||
groupedLockInfos = append(groupedLockInfos, volLocks...)
|
|
||||||
}
|
|
||||||
return groupedLockInfos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// uptimeSlice - used to sort uptimes in chronological order.
|
// uptimeSlice - used to sort uptimes in chronological order.
|
||||||
type uptimeSlice []struct {
|
type uptimeSlice []struct {
|
||||||
err error
|
err error
|
||||||
@@ -365,187 +275,3 @@ func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
|||||||
|
|
||||||
return latestUptime, nil
|
return latestUptime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPeerConfig - Fetches config.json from all nodes in the setup and
|
|
||||||
// returns the one that occurs in a majority of them.
|
|
||||||
func getPeerConfig(peers adminPeers) ([]byte, error) {
|
|
||||||
if !globalIsDistXL {
|
|
||||||
return peers[0].cmdRunner.GetConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
configs := make([][]byte, len(peers))
|
|
||||||
|
|
||||||
// Get config from all servers.
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for i, peer := range peers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, peer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
configs[idx], errs[idx] = peer.cmdRunner.GetConfig()
|
|
||||||
}(i, peer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Find the maximally occurring config among peers in a
|
|
||||||
// distributed setup.
|
|
||||||
|
|
||||||
serverConfigs := make([]serverConfig, len(peers))
|
|
||||||
for i, configBytes := range configs {
|
|
||||||
if errs[i] != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal the received config files.
|
|
||||||
err := json.Unmarshal(configBytes, &serverConfigs[i])
|
|
||||||
if err != nil {
|
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", peers[i].addr)
|
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configJSON, err := getValidServerConfig(serverConfigs, errs)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the config.json that was present quorum or more
|
|
||||||
// number of disks.
|
|
||||||
return json.Marshal(configJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getValidServerConfig - finds the server config that is present in
|
|
||||||
// quorum or more number of servers.
|
|
||||||
func getValidServerConfig(serverConfigs []serverConfig, errs []error) (scv serverConfig, e error) {
|
|
||||||
// majority-based quorum
|
|
||||||
quorum := len(serverConfigs)/2 + 1
|
|
||||||
|
|
||||||
// Count the number of disks a config.json was found in.
|
|
||||||
configCounter := make([]int, len(serverConfigs))
|
|
||||||
|
|
||||||
// We group equal serverConfigs by the lowest index of the
|
|
||||||
// same value; e.g, let us take the following serverConfigs
|
|
||||||
// in a 4-node setup,
|
|
||||||
// serverConfigs == [c1, c2, c1, c1]
|
|
||||||
// configCounter == [3, 1, 0, 0]
|
|
||||||
// c1, c2 are the only distinct values that appear. c1 is
|
|
||||||
// identified by 0, the lowest index it appears in and c2 is
|
|
||||||
// identified by 1. So, we need to find the number of times
|
|
||||||
// each of these distinct values occur.
|
|
||||||
|
|
||||||
// Invariants:
|
|
||||||
|
|
||||||
// 1. At the beginning of the i-th iteration, the number of
|
|
||||||
// unique configurations seen so far is equal to the number of
|
|
||||||
// non-zero counter values in config[:i].
|
|
||||||
|
|
||||||
// 2. At the beginning of the i-th iteration, the sum of
|
|
||||||
// elements of configCounter[:i] is equal to the number of
|
|
||||||
// non-error configurations seen so far.
|
|
||||||
|
|
||||||
// For each of the serverConfig ...
|
|
||||||
for i := range serverConfigs {
|
|
||||||
// Skip nodes where getConfig failed.
|
|
||||||
if errs[i] != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Check if it is equal to any of the configurations
|
|
||||||
// seen so far. If j == i is reached then we have an
|
|
||||||
// unseen configuration.
|
|
||||||
for j := 0; j <= i; j++ {
|
|
||||||
if j < i && configCounter[j] == 0 {
|
|
||||||
// serverConfigs[j] is known to be
|
|
||||||
// equal to a value that was already
|
|
||||||
// seen. See example above for
|
|
||||||
// clarity.
|
|
||||||
continue
|
|
||||||
} else if j < i && serverConfigs[i].ConfigDiff(&serverConfigs[j]) == "" {
|
|
||||||
// serverConfigs[i] is equal to
|
|
||||||
// serverConfigs[j], update
|
|
||||||
// serverConfigs[j]'s counter since it
|
|
||||||
// is the lower index.
|
|
||||||
configCounter[j]++
|
|
||||||
break
|
|
||||||
} else if j == i {
|
|
||||||
// serverConfigs[i] is equal to no
|
|
||||||
// other value seen before. It is
|
|
||||||
// unique so far.
|
|
||||||
configCounter[i] = 1
|
|
||||||
break
|
|
||||||
} // else invariants specified above are violated.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We find the maximally occurring server config and check if
|
|
||||||
// there is quorum.
|
|
||||||
var configJSON serverConfig
|
|
||||||
maxOccurrence := 0
|
|
||||||
for i, count := range configCounter {
|
|
||||||
if maxOccurrence < count {
|
|
||||||
maxOccurrence = count
|
|
||||||
configJSON = serverConfigs[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If quorum nodes don't agree.
|
|
||||||
if maxOccurrence < quorum {
|
|
||||||
return scv, errXLWriteQuorum
|
|
||||||
}
|
|
||||||
|
|
||||||
return configJSON, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write config contents into a temporary file on all nodes.
|
|
||||||
func writeTmpConfigPeers(peers adminPeers, tmpFileName string, configBytes []byte) []error {
|
|
||||||
// For a single-node minio server setup.
|
|
||||||
if !globalIsDistXL {
|
|
||||||
err := peers[0].cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
|
||||||
return []error{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
|
|
||||||
// Write config into temporary file on all nodes.
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for i, peer := range peers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, peer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
errs[idx] = peer.cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
|
||||||
}(i, peer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Return bytes written and errors (if any) during writing
|
|
||||||
// temporary config file.
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move config contents from the given temporary file onto config.json
|
|
||||||
// on all nodes.
|
|
||||||
func commitConfigPeers(peers adminPeers, tmpFileName string) []error {
|
|
||||||
// For a single-node minio server setup.
|
|
||||||
if !globalIsDistXL {
|
|
||||||
return []error{peers[0].cmdRunner.CommitConfig(tmpFileName)}
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
|
|
||||||
// Rename temporary config file into configDir/config.json on
|
|
||||||
// all nodes.
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for i, peer := range peers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, peer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
errs[idx] = peer.cmdRunner.CommitConfig(tmpFileName)
|
|
||||||
}(i, peer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Return errors (if any) received during rename.
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,650 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2014, 2015, 2016, 2017, 2018 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/minio/minio-go/pkg/set"
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Admin service names
|
|
||||||
signalServiceRPC = "Admin.SignalService"
|
|
||||||
reInitFormatRPC = "Admin.ReInitFormat"
|
|
||||||
listLocksRPC = "Admin.ListLocks"
|
|
||||||
serverInfoDataRPC = "Admin.ServerInfoData"
|
|
||||||
getConfigRPC = "Admin.GetConfig"
|
|
||||||
writeTmpConfigRPC = "Admin.WriteTmpConfig"
|
|
||||||
commitConfigRPC = "Admin.CommitConfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// localAdminClient - represents admin operation to be executed locally.
|
|
||||||
type localAdminClient struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// remoteAdminClient - represents admin operation to be executed
|
|
||||||
// remotely, via RPC.
|
|
||||||
type remoteAdminClient struct {
|
|
||||||
*AuthRPCClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// adminCmdRunner - abstracts local and remote execution of admin
|
|
||||||
// commands like service stop and service restart.
|
|
||||||
type adminCmdRunner interface {
|
|
||||||
SignalService(s serviceSignal) error
|
|
||||||
ReInitFormat(dryRun bool) error
|
|
||||||
ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error)
|
|
||||||
ServerInfoData() (ServerInfoData, error)
|
|
||||||
GetConfig() ([]byte, error)
|
|
||||||
WriteTmpConfig(tmpFileName string, configBytes []byte) error
|
|
||||||
CommitConfig(tmpFileName string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
var errUnsupportedSignal = fmt.Errorf("unsupported signal: only restart and stop signals are supported")
|
|
||||||
|
|
||||||
// SignalService - sends a restart or stop signal to the local server
|
|
||||||
func (lc localAdminClient) SignalService(s serviceSignal) error {
|
|
||||||
switch s {
|
|
||||||
case serviceRestart, serviceStop:
|
|
||||||
globalServiceSignalCh <- s
|
|
||||||
default:
|
|
||||||
return errUnsupportedSignal
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReInitFormat - re-initialize disk format.
|
|
||||||
func (lc localAdminClient) ReInitFormat(dryRun bool) error {
|
|
||||||
objectAPI := newObjectLayerFn()
|
|
||||||
if objectAPI == nil {
|
|
||||||
return errServerNotInitialized
|
|
||||||
}
|
|
||||||
return objectAPI.ReloadFormat(context.Background(), dryRun)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListLocks - Fetches lock information from local lock instrumentation.
|
|
||||||
func (lc localAdminClient) ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
|
||||||
// check if objectLayer is initialized, if not return.
|
|
||||||
objectAPI := newObjectLayerFn()
|
|
||||||
if objectAPI == nil {
|
|
||||||
return nil, errServerNotInitialized
|
|
||||||
}
|
|
||||||
return objectAPI.ListLocks(context.Background(), bucket, prefix, duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc remoteAdminClient) SignalService(s serviceSignal) (err error) {
|
|
||||||
switch s {
|
|
||||||
case serviceRestart, serviceStop:
|
|
||||||
reply := AuthRPCReply{}
|
|
||||||
err = rc.Call(signalServiceRPC, &SignalServiceArgs{Sig: s},
|
|
||||||
&reply)
|
|
||||||
default:
|
|
||||||
err = errUnsupportedSignal
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReInitFormat - re-initialize disk format, remotely.
|
|
||||||
func (rc remoteAdminClient) ReInitFormat(dryRun bool) error {
|
|
||||||
reply := AuthRPCReply{}
|
|
||||||
return rc.Call(reInitFormatRPC, &ReInitFormatArgs{
|
|
||||||
DryRun: dryRun,
|
|
||||||
}, &reply)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListLocks - Sends list locks command to remote server via RPC.
|
|
||||||
func (rc remoteAdminClient) ListLocks(bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
|
||||||
listArgs := ListLocksQuery{
|
|
||||||
Bucket: bucket,
|
|
||||||
Prefix: prefix,
|
|
||||||
Duration: duration,
|
|
||||||
}
|
|
||||||
var reply ListLocksReply
|
|
||||||
if err := rc.Call(listLocksRPC, &listArgs, &reply); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reply.VolLocks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerInfoData - Returns the server info of this server.
|
|
||||||
func (lc localAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
|
||||||
if globalBootTime.IsZero() {
|
|
||||||
return sid, errServerNotInitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build storage info
|
|
||||||
objLayer := newObjectLayerFn()
|
|
||||||
if objLayer == nil {
|
|
||||||
return sid, errServerNotInitialized
|
|
||||||
}
|
|
||||||
storage := objLayer.StorageInfo(context.Background())
|
|
||||||
|
|
||||||
return ServerInfoData{
|
|
||||||
StorageInfo: storage,
|
|
||||||
ConnStats: globalConnStats.toServerConnStats(),
|
|
||||||
HTTPStats: globalHTTPStats.toServerHTTPStats(),
|
|
||||||
Properties: ServerProperties{
|
|
||||||
Uptime: UTCNow().Sub(globalBootTime),
|
|
||||||
Version: Version,
|
|
||||||
CommitID: CommitID,
|
|
||||||
SQSARN: globalNotificationSys.GetARNList(),
|
|
||||||
Region: globalServerConfig.GetRegion(),
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerInfo - returns the server info of the server to which the RPC call is made.
|
|
||||||
func (rc remoteAdminClient) ServerInfoData() (sid ServerInfoData, e error) {
|
|
||||||
args := AuthRPCArgs{}
|
|
||||||
reply := ServerInfoDataReply{}
|
|
||||||
err := rc.Call(serverInfoDataRPC, &args, &reply)
|
|
||||||
if err != nil {
|
|
||||||
return sid, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply.ServerInfoData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfig - returns config.json of the local server.
|
|
||||||
func (lc localAdminClient) GetConfig() ([]byte, error) {
|
|
||||||
if globalServerConfig == nil {
|
|
||||||
return nil, fmt.Errorf("config not present")
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(globalServerConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfig - returns config.json of the remote server.
|
|
||||||
func (rc remoteAdminClient) GetConfig() ([]byte, error) {
|
|
||||||
args := AuthRPCArgs{}
|
|
||||||
reply := ConfigReply{}
|
|
||||||
if err := rc.Call(getConfigRPC, &args, &reply); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reply.Config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTmpConfig - writes config file content to a temporary file on
|
|
||||||
// the local server.
|
|
||||||
func (lc localAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
|
|
||||||
return writeTmpConfigCommon(tmpFileName, configBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTmpConfig - writes config file content to a temporary file on
|
|
||||||
// a remote node.
|
|
||||||
func (rc remoteAdminClient) WriteTmpConfig(tmpFileName string, configBytes []byte) error {
|
|
||||||
wArgs := WriteConfigArgs{
|
|
||||||
TmpFileName: tmpFileName,
|
|
||||||
Buf: configBytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := rc.Call(writeTmpConfigRPC, &wArgs, &WriteConfigReply{})
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitConfig - Move the new config in tmpFileName onto config.json
|
|
||||||
// on a local node.
|
|
||||||
func (lc localAdminClient) CommitConfig(tmpFileName string) error {
|
|
||||||
configFile := getConfigFile()
|
|
||||||
tmpConfigFile := filepath.Join(getConfigDir(), tmpFileName)
|
|
||||||
|
|
||||||
err := os.Rename(tmpConfigFile, configFile)
|
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("tmpConfigFile", tmpConfigFile)
|
|
||||||
reqInfo.AppendTags("configFile", configFile)
|
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitConfig - Move the new config in tmpFileName onto config.json
|
|
||||||
// on a remote node.
|
|
||||||
func (rc remoteAdminClient) CommitConfig(tmpFileName string) error {
|
|
||||||
cArgs := CommitConfigArgs{
|
|
||||||
FileName: tmpFileName,
|
|
||||||
}
|
|
||||||
cReply := CommitConfigReply{}
|
|
||||||
err := rc.Call(commitConfigRPC, &cArgs, &cReply)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// adminPeer - represents an entity that implements admin API RPCs.
|
|
||||||
type adminPeer struct {
|
|
||||||
addr string
|
|
||||||
cmdRunner adminCmdRunner
|
|
||||||
isLocal bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// type alias for a collection of adminPeer.
|
|
||||||
type adminPeers []adminPeer
|
|
||||||
|
|
||||||
// makeAdminPeers - helper function to construct a collection of adminPeer.
|
|
||||||
func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
|
||||||
thisPeer := globalMinioAddr
|
|
||||||
if globalMinioHost == "" {
|
|
||||||
// When host is not explicitly provided simply
|
|
||||||
// use the first IPv4.
|
|
||||||
thisPeer = net.JoinHostPort(sortIPs(localIP4.ToSlice())[0], globalMinioPort)
|
|
||||||
}
|
|
||||||
adminPeerList = append(adminPeerList, adminPeer{
|
|
||||||
thisPeer,
|
|
||||||
localAdminClient{},
|
|
||||||
true,
|
|
||||||
})
|
|
||||||
|
|
||||||
hostSet := set.CreateStringSet(globalMinioAddr)
|
|
||||||
cred := globalServerConfig.GetCredential()
|
|
||||||
serviceEndpoint := path.Join(minioReservedBucketPath, adminPath)
|
|
||||||
for _, host := range GetRemotePeers(endpoints) {
|
|
||||||
if hostSet.Contains(host) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hostSet.Add(host)
|
|
||||||
adminPeerList = append(adminPeerList, adminPeer{
|
|
||||||
addr: host,
|
|
||||||
cmdRunner: &remoteAdminClient{newAuthRPCClient(authConfig{
|
|
||||||
accessKey: cred.AccessKey,
|
|
||||||
secretKey: cred.SecretKey,
|
|
||||||
serverAddr: host,
|
|
||||||
serviceEndpoint: serviceEndpoint,
|
|
||||||
secureConn: globalIsSSL,
|
|
||||||
serviceName: "Admin",
|
|
||||||
})},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return adminPeerList
|
|
||||||
}
|
|
||||||
|
|
||||||
// peersReInitFormat - reinitialize remote object layers to new format.
|
|
||||||
func peersReInitFormat(peers adminPeers, dryRun bool) error {
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
|
|
||||||
// Send ReInitFormat RPC call to all nodes.
|
|
||||||
// for local adminPeer this is a no-op.
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for i, peer := range peers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, peer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
if !peer.isLocal {
|
|
||||||
errs[idx] = peer.cmdRunner.ReInitFormat(dryRun)
|
|
||||||
}
|
|
||||||
}(i, peer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize global adminPeer collection.
|
|
||||||
func initGlobalAdminPeers(endpoints EndpointList) {
|
|
||||||
globalAdminPeers = makeAdminPeers(endpoints)
|
|
||||||
}
|
|
||||||
|
|
||||||
// invokeServiceCmd - Invoke Restart/Stop command.
|
|
||||||
func invokeServiceCmd(cp adminPeer, cmd serviceSignal) (err error) {
|
|
||||||
switch cmd {
|
|
||||||
case serviceRestart, serviceStop:
|
|
||||||
err = cp.cmdRunner.SignalService(cmd)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendServiceCmd - Invoke Restart command on remote peers
|
|
||||||
// adminPeer followed by on the local peer.
|
|
||||||
func sendServiceCmd(cps adminPeers, cmd serviceSignal) {
|
|
||||||
// Send service command like stop or restart to all remote nodes and finally run on local node.
|
|
||||||
errs := make([]error, len(cps))
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
remotePeers := cps[1:]
|
|
||||||
for i := range remotePeers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int) {
|
|
||||||
defer wg.Done()
|
|
||||||
// we use idx+1 because remotePeers slice is 1 position shifted w.r.t cps
|
|
||||||
errs[idx+1] = invokeServiceCmd(remotePeers[idx], cmd)
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
errs[0] = invokeServiceCmd(cps[0], cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// listPeerLocksInfo - fetch list of locks held on the given bucket,
|
|
||||||
// matching prefix held longer than duration from all peer servers.
|
|
||||||
func listPeerLocksInfo(peers adminPeers, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
|
||||||
// Used to aggregate volume lock information from all nodes.
|
|
||||||
allLocks := make([][]VolumeLockInfo, len(peers))
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
localPeer := peers[0]
|
|
||||||
remotePeers := peers[1:]
|
|
||||||
for i, remotePeer := range remotePeers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, remotePeer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
// `remotePeers` is right-shifted by one position relative to `peers`
|
|
||||||
allLocks[idx], errs[idx] = remotePeer.cmdRunner.ListLocks(bucket, prefix, duration)
|
|
||||||
}(i+1, remotePeer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
allLocks[0], errs[0] = localPeer.cmdRunner.ListLocks(bucket, prefix, duration)
|
|
||||||
|
|
||||||
// Summarizing errors received for ListLocks RPC across all
|
|
||||||
// nodes. N B the possible unavailability of quorum in errors
|
|
||||||
// applies only to distributed setup.
|
|
||||||
errCount, err := reduceErrs(errs, []error{})
|
|
||||||
if err != nil {
|
|
||||||
if errCount >= (len(peers)/2 + 1) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, InsufficientReadQuorum{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group lock information across nodes by (bucket, object)
|
|
||||||
// pair. For readability only.
|
|
||||||
paramLockMap := make(map[nsParam][]VolumeLockInfo)
|
|
||||||
for _, nodeLocks := range allLocks {
|
|
||||||
for _, lockInfo := range nodeLocks {
|
|
||||||
param := nsParam{
|
|
||||||
volume: lockInfo.Bucket,
|
|
||||||
path: lockInfo.Object,
|
|
||||||
}
|
|
||||||
paramLockMap[param] = append(paramLockMap[param], lockInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
groupedLockInfos := []VolumeLockInfo{}
|
|
||||||
for _, volLocks := range paramLockMap {
|
|
||||||
groupedLockInfos = append(groupedLockInfos, volLocks...)
|
|
||||||
}
|
|
||||||
return groupedLockInfos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// uptimeSlice - used to sort uptimes in chronological order.
|
|
||||||
type uptimeSlice []struct {
|
|
||||||
err error
|
|
||||||
uptime time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts uptimeSlice) Len() int {
|
|
||||||
return len(ts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts uptimeSlice) Less(i, j int) bool {
|
|
||||||
return ts[i].uptime < ts[j].uptime
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts uptimeSlice) Swap(i, j int) {
|
|
||||||
ts[i], ts[j] = ts[j], ts[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPeerUptimes - returns the uptime since the last time read quorum
|
|
||||||
// was established on success. Otherwise returns errXLReadQuorum.
|
|
||||||
func getPeerUptimes(peers adminPeers) (time.Duration, error) {
|
|
||||||
// In a single node Erasure or FS backend setup the uptime of
|
|
||||||
// the setup is the uptime of the single minio server
|
|
||||||
// instance.
|
|
||||||
if !globalIsDistXL {
|
|
||||||
return UTCNow().Sub(globalBootTime), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
uptimes := make(uptimeSlice, len(peers))
|
|
||||||
|
|
||||||
// Get up time of all servers.
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for i, peer := range peers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, peer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
serverInfoData, rpcErr := peer.cmdRunner.ServerInfoData()
|
|
||||||
uptimes[idx].uptime, uptimes[idx].err = serverInfoData.Properties.Uptime, rpcErr
|
|
||||||
}(i, peer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Sort uptimes in chronological order.
|
|
||||||
sort.Sort(uptimes)
|
|
||||||
|
|
||||||
// Pick the readQuorum'th uptime in chronological order. i.e,
|
|
||||||
// the time at which read quorum was (re-)established.
|
|
||||||
readQuorum := len(uptimes) / 2
|
|
||||||
validCount := 0
|
|
||||||
latestUptime := time.Duration(0)
|
|
||||||
for _, uptime := range uptimes {
|
|
||||||
if uptime.err != nil {
|
|
||||||
logger.LogIf(context.Background(), uptime.err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
validCount++
|
|
||||||
if validCount >= readQuorum {
|
|
||||||
latestUptime = uptime.uptime
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Less than readQuorum "Admin.Uptime" RPC call returned
|
|
||||||
// successfully, so read-quorum unavailable.
|
|
||||||
if validCount < readQuorum {
|
|
||||||
return time.Duration(0), InsufficientReadQuorum{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestUptime, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPeerConfig - Fetches config.json from all nodes in the setup and
|
|
||||||
// returns the one that occurs in a majority of them.
|
|
||||||
func getPeerConfig(peers adminPeers) ([]byte, error) {
|
|
||||||
if !globalIsDistXL {
|
|
||||||
return peers[0].cmdRunner.GetConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
configs := make([][]byte, len(peers))
|
|
||||||
|
|
||||||
// Get config from all servers.
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for i, peer := range peers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, peer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
configs[idx], errs[idx] = peer.cmdRunner.GetConfig()
|
|
||||||
}(i, peer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Find the maximally occurring config among peers in a
|
|
||||||
// distributed setup.
|
|
||||||
|
|
||||||
serverConfigs := make([]serverConfig, len(peers))
|
|
||||||
for i, configBytes := range configs {
|
|
||||||
if errs[i] != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal the received config files.
|
|
||||||
err := json.Unmarshal(configBytes, &serverConfigs[i])
|
|
||||||
if err != nil {
|
|
||||||
reqInfo := (&logger.ReqInfo{}).AppendTags("peerAddress", peers[i].addr)
|
|
||||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configJSON, err := getValidServerConfig(serverConfigs, errs)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(context.Background(), err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the config.json that was present quorum or more
|
|
||||||
// number of disks.
|
|
||||||
return json.Marshal(configJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getValidServerConfig - finds the server config that is present in
|
|
||||||
// quorum or more number of servers.
|
|
||||||
func getValidServerConfig(serverConfigs []serverConfig, errs []error) (scv serverConfig, e error) {
|
|
||||||
// majority-based quorum
|
|
||||||
quorum := len(serverConfigs)/2 + 1
|
|
||||||
|
|
||||||
// Count the number of disks a config.json was found in.
|
|
||||||
configCounter := make([]int, len(serverConfigs))
|
|
||||||
|
|
||||||
// We group equal serverConfigs by the lowest index of the
|
|
||||||
// same value; e.g, let us take the following serverConfigs
|
|
||||||
// in a 4-node setup,
|
|
||||||
// serverConfigs == [c1, c2, c1, c1]
|
|
||||||
// configCounter == [3, 1, 0, 0]
|
|
||||||
// c1, c2 are the only distinct values that appear. c1 is
|
|
||||||
// identified by 0, the lowest index it appears in and c2 is
|
|
||||||
// identified by 1. So, we need to find the number of times
|
|
||||||
// each of these distinct values occur.
|
|
||||||
|
|
||||||
// Invariants:
|
|
||||||
|
|
||||||
// 1. At the beginning of the i-th iteration, the number of
|
|
||||||
// unique configurations seen so far is equal to the number of
|
|
||||||
// non-zero counter values in config[:i].
|
|
||||||
|
|
||||||
// 2. At the beginning of the i-th iteration, the sum of
|
|
||||||
// elements of configCounter[:i] is equal to the number of
|
|
||||||
// non-error configurations seen so far.
|
|
||||||
|
|
||||||
// For each of the serverConfig ...
|
|
||||||
for i := range serverConfigs {
|
|
||||||
// Skip nodes where getConfig failed.
|
|
||||||
if errs[i] != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Check if it is equal to any of the configurations
|
|
||||||
// seen so far. If j == i is reached then we have an
|
|
||||||
// unseen configuration.
|
|
||||||
for j := 0; j <= i; j++ {
|
|
||||||
if j < i && configCounter[j] == 0 {
|
|
||||||
// serverConfigs[j] is known to be
|
|
||||||
// equal to a value that was already
|
|
||||||
// seen. See example above for
|
|
||||||
// clarity.
|
|
||||||
continue
|
|
||||||
} else if j < i && serverConfigs[i].ConfigDiff(&serverConfigs[j]) == "" {
|
|
||||||
// serverConfigs[i] is equal to
|
|
||||||
// serverConfigs[j], update
|
|
||||||
// serverConfigs[j]'s counter since it
|
|
||||||
// is the lower index.
|
|
||||||
configCounter[j]++
|
|
||||||
break
|
|
||||||
} else if j == i {
|
|
||||||
// serverConfigs[i] is equal to no
|
|
||||||
// other value seen before. It is
|
|
||||||
// unique so far.
|
|
||||||
configCounter[i] = 1
|
|
||||||
break
|
|
||||||
} // else invariants specified above are violated.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We find the maximally occurring server config and check if
|
|
||||||
// there is quorum.
|
|
||||||
var configJSON serverConfig
|
|
||||||
maxOccurrence := 0
|
|
||||||
for i, count := range configCounter {
|
|
||||||
if maxOccurrence < count {
|
|
||||||
maxOccurrence = count
|
|
||||||
configJSON = serverConfigs[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If quorum nodes don't agree.
|
|
||||||
if maxOccurrence < quorum {
|
|
||||||
return scv, errXLWriteQuorum
|
|
||||||
}
|
|
||||||
|
|
||||||
return configJSON, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write config contents into a temporary file on all nodes.
|
|
||||||
func writeTmpConfigPeers(peers adminPeers, tmpFileName string, configBytes []byte) []error {
|
|
||||||
// For a single-node minio server setup.
|
|
||||||
if !globalIsDistXL {
|
|
||||||
err := peers[0].cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
|
||||||
return []error{err}
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
|
|
||||||
// Write config into temporary file on all nodes.
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for i, peer := range peers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, peer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
errs[idx] = peer.cmdRunner.WriteTmpConfig(tmpFileName, configBytes)
|
|
||||||
}(i, peer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Return bytes written and errors (if any) during writing
|
|
||||||
// temporary config file.
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move config contents from the given temporary file onto config.json
|
|
||||||
// on all nodes.
|
|
||||||
func commitConfigPeers(peers adminPeers, tmpFileName string) []error {
|
|
||||||
// For a single-node minio server setup.
|
|
||||||
if !globalIsDistXL {
|
|
||||||
return []error{peers[0].cmdRunner.CommitConfig(tmpFileName)}
|
|
||||||
}
|
|
||||||
|
|
||||||
errs := make([]error, len(peers))
|
|
||||||
|
|
||||||
// Rename temporary config file into configDir/config.json on
|
|
||||||
// all nodes.
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
for i, peer := range peers {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, peer adminPeer) {
|
|
||||||
defer wg.Done()
|
|
||||||
errs[idx] = peer.cmdRunner.CommitConfig(tmpFileName)
|
|
||||||
}(i, peer)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Return errors (if any) received during rename.
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
+1
-41
@@ -19,7 +19,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
@@ -47,20 +46,6 @@ func (receiver *adminRPCReceiver) SignalService(args *SignalServiceArgs, reply *
|
|||||||
return receiver.local.SignalService(args.Sig)
|
return receiver.local.SignalService(args.Sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListLocksQuery - wraps ListLocks API's query values to send over RPC.
|
|
||||||
type ListLocksQuery struct {
|
|
||||||
AuthArgs
|
|
||||||
Bucket string
|
|
||||||
Prefix string
|
|
||||||
Duration time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListLocks - lists locks held by requests handled by this server instance.
|
|
||||||
func (receiver *adminRPCReceiver) ListLocks(args *ListLocksQuery, reply *[]VolumeLockInfo) (err error) {
|
|
||||||
*reply, err = receiver.local.ListLocks(args.Bucket, args.Prefix, args.Duration)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServerInfo - returns the server info when object layer was initialized on this server.
|
// ServerInfo - returns the server info when object layer was initialized on this server.
|
||||||
func (receiver *adminRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoData) (err error) {
|
func (receiver *adminRPCReceiver) ServerInfo(args *AuthArgs, reply *ServerInfoData) (err error) {
|
||||||
*reply, err = receiver.local.ServerInfo()
|
*reply, err = receiver.local.ServerInfo()
|
||||||
@@ -84,31 +69,6 @@ func (receiver *adminRPCReceiver) ReInitFormat(args *ReInitFormatArgs, reply *Vo
|
|||||||
return receiver.local.ReInitFormat(args.DryRun)
|
return receiver.local.ReInitFormat(args.DryRun)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteConfigArgs - wraps the bytes to be written and temporary file name.
|
|
||||||
type WriteConfigArgs struct {
|
|
||||||
AuthArgs
|
|
||||||
TmpFileName string
|
|
||||||
Buf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTmpConfig - writes the supplied config contents onto the
|
|
||||||
// supplied temporary file.
|
|
||||||
func (receiver *adminRPCReceiver) WriteTmpConfig(args *WriteConfigArgs, reply *VoidReply) error {
|
|
||||||
return receiver.local.WriteTmpConfig(args.TmpFileName, args.Buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitConfigArgs - wraps the config file name that needs to be
|
|
||||||
// committed into config.json on this node.
|
|
||||||
type CommitConfigArgs struct {
|
|
||||||
AuthArgs
|
|
||||||
FileName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitConfig - Renames the temporary file into config.json on this node.
|
|
||||||
func (receiver *adminRPCReceiver) CommitConfig(args *CommitConfigArgs, reply *VoidReply) error {
|
|
||||||
return receiver.local.CommitConfig(args.FileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAdminRPCServer - returns new admin RPC server.
|
// NewAdminRPCServer - returns new admin RPC server.
|
||||||
func NewAdminRPCServer() (*xrpc.Server, error) {
|
func NewAdminRPCServer() (*xrpc.Server, error) {
|
||||||
rpcServer := xrpc.NewServer()
|
rpcServer := xrpc.NewServer()
|
||||||
@@ -123,5 +83,5 @@ func registerAdminRPCRouter(router *mux.Router) {
|
|||||||
rpcServer, err := NewAdminRPCServer()
|
rpcServer, err := NewAdminRPCServer()
|
||||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server", context.Background())
|
logger.FatalIf(err, "Unable to initialize Lock RPC Server", context.Background())
|
||||||
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
||||||
subrouter.Path(adminServiceSubPath).Handler(rpcServer)
|
subrouter.Path(adminServiceSubPath).HandlerFunc(httpTraceHdrs(rpcServer.ServeHTTP))
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-376
@@ -17,13 +17,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -99,31 +94,6 @@ func testAdminCmdRunnerReInitFormat(t *testing.T, client adminCmdRunner) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAdminCmdRunnerListLocks(t *testing.T, client adminCmdRunner) {
|
|
||||||
tmpGlobalObjectAPI := globalObjectAPI
|
|
||||||
defer func() {
|
|
||||||
globalObjectAPI = tmpGlobalObjectAPI
|
|
||||||
}()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
objectAPI ObjectLayer
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{&DummyObjectLayer{}, false},
|
|
||||||
{nil, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
globalObjectAPI = testCase.objectAPI
|
|
||||||
_, err := client.ListLocks("", "", time.Duration(0))
|
|
||||||
expectErr := (err != nil)
|
|
||||||
|
|
||||||
if expectErr != testCase.expectErr {
|
|
||||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
||||||
tmpGlobalBootTime := globalBootTime
|
tmpGlobalBootTime := globalBootTime
|
||||||
tmpGlobalObjectAPI := globalObjectAPI
|
tmpGlobalObjectAPI := globalObjectAPI
|
||||||
@@ -139,10 +109,8 @@ func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
endpoints := new(EndpointList)
|
endpoints := new(EndpointList)
|
||||||
notificationSys, err := NewNotificationSys(globalServerConfig, *endpoints)
|
|
||||||
if err != nil {
|
notificationSys := NewNotificationSys(globalServerConfig, *endpoints)
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
bootTime time.Time
|
bootTime time.Time
|
||||||
@@ -199,81 +167,6 @@ func testAdminCmdRunnerGetConfig(t *testing.T, client adminCmdRunner) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAdminCmdRunnerWriteTmpConfig(t *testing.T, client adminCmdRunner) {
|
|
||||||
tmpConfigDir := configDir
|
|
||||||
defer func() {
|
|
||||||
configDir = tmpConfigDir
|
|
||||||
}()
|
|
||||||
|
|
||||||
tempDir, err := ioutil.TempDir("", ".AdminCmdRunnerWriteTmpConfig.")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
configDir = &ConfigDir{dir: tempDir}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
tmpFilename string
|
|
||||||
configBytes []byte
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{"config1.json", []byte(`{"version":"23","region":"us-west-1a"}`), false},
|
|
||||||
// Overwrite test.
|
|
||||||
{"config1.json", []byte(`{"version":"23","region":"us-west-1a","browser":"on"}`), false},
|
|
||||||
{"config2.json", []byte{}, false},
|
|
||||||
{"config3.json", nil, false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
err := client.WriteTmpConfig(testCase.tmpFilename, testCase.configBytes)
|
|
||||||
expectErr := (err != nil)
|
|
||||||
|
|
||||||
if expectErr != testCase.expectErr {
|
|
||||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testAdminCmdRunnerCommitConfig(t *testing.T, client adminCmdRunner) {
|
|
||||||
tmpConfigDir := configDir
|
|
||||||
defer func() {
|
|
||||||
configDir = tmpConfigDir
|
|
||||||
}()
|
|
||||||
|
|
||||||
tempDir, err := ioutil.TempDir("", ".AdminCmdRunnerCommitConfig.")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
configDir = &ConfigDir{dir: tempDir}
|
|
||||||
err = ioutil.WriteFile(filepath.Join(tempDir, "config.json"), []byte{}, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = client.WriteTmpConfig("config1.json", []byte(`{"version":"23","region":"us-west-1a"}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
tmpFilename string
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{"config1.json", false},
|
|
||||||
{"config2.json", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
err := client.CommitConfig(testCase.tmpFilename)
|
|
||||||
expectErr := (err != nil)
|
|
||||||
|
|
||||||
if expectErr != testCase.expectErr {
|
|
||||||
t.Fatalf("case %v: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) {
|
func newAdminRPCHTTPServerClient(t *testing.T) (*httptest.Server, *AdminRPCClient, *serverConfig) {
|
||||||
rpcServer, err := NewAdminRPCServer()
|
rpcServer, err := NewAdminRPCServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -325,16 +218,6 @@ func TestAdminRPCClientReInitFormat(t *testing.T) {
|
|||||||
testAdminCmdRunnerReInitFormat(t, rpcClient)
|
testAdminCmdRunnerReInitFormat(t, rpcClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminRPCClientListLocks(t *testing.T) {
|
|
||||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
|
||||||
defer httpServer.Close()
|
|
||||||
defer func() {
|
|
||||||
globalServerConfig = prevGlobalServerConfig
|
|
||||||
}()
|
|
||||||
|
|
||||||
testAdminCmdRunnerListLocks(t, rpcClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdminRPCClientServerInfo(t *testing.T) {
|
func TestAdminRPCClientServerInfo(t *testing.T) {
|
||||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
||||||
defer httpServer.Close()
|
defer httpServer.Close()
|
||||||
@@ -354,260 +237,3 @@ func TestAdminRPCClientGetConfig(t *testing.T) {
|
|||||||
|
|
||||||
testAdminCmdRunnerGetConfig(t, rpcClient)
|
testAdminCmdRunnerGetConfig(t, rpcClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminRPCClientWriteTmpConfig(t *testing.T) {
|
|
||||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
|
||||||
defer httpServer.Close()
|
|
||||||
defer func() {
|
|
||||||
globalServerConfig = prevGlobalServerConfig
|
|
||||||
}()
|
|
||||||
|
|
||||||
testAdminCmdRunnerWriteTmpConfig(t, rpcClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdminRPCClientCommitConfig(t *testing.T) {
|
|
||||||
httpServer, rpcClient, prevGlobalServerConfig := newAdminRPCHTTPServerClient(t)
|
|
||||||
defer httpServer.Close()
|
|
||||||
defer func() {
|
|
||||||
globalServerConfig = prevGlobalServerConfig
|
|
||||||
}()
|
|
||||||
|
|
||||||
testAdminCmdRunnerCommitConfig(t, rpcClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
config1 = []byte(`{
|
|
||||||
"version": "13",
|
|
||||||
"credential": {
|
|
||||||
"accessKey": "minio",
|
|
||||||
"secretKey": "minio123"
|
|
||||||
},
|
|
||||||
"region": "us-east-1",
|
|
||||||
"logger": {
|
|
||||||
"console": {
|
|
||||||
"enable": true,
|
|
||||||
"level": "debug"
|
|
||||||
},
|
|
||||||
"file": {
|
|
||||||
"enable": false,
|
|
||||||
"fileName": "",
|
|
||||||
"level": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notify": {
|
|
||||||
"amqp": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"url": "",
|
|
||||||
"exchange": "",
|
|
||||||
"routingKey": "",
|
|
||||||
"exchangeType": "",
|
|
||||||
"mandatory": false,
|
|
||||||
"immediate": false,
|
|
||||||
"durable": false,
|
|
||||||
"internal": false,
|
|
||||||
"noWait": false,
|
|
||||||
"autoDeleted": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nats": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"address": "",
|
|
||||||
"subject": "",
|
|
||||||
"username": "",
|
|
||||||
"password": "",
|
|
||||||
"token": "",
|
|
||||||
"secure": false,
|
|
||||||
"pingInterval": 0,
|
|
||||||
"streaming": {
|
|
||||||
"enable": false,
|
|
||||||
"clusterID": "",
|
|
||||||
"clientID": "",
|
|
||||||
"async": false,
|
|
||||||
"maxPubAcksInflight": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"elasticsearch": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"url": "",
|
|
||||||
"index": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"redis": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"address": "",
|
|
||||||
"password": "",
|
|
||||||
"key": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"postgresql": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"connectionString": "",
|
|
||||||
"table": "",
|
|
||||||
"host": "",
|
|
||||||
"port": "",
|
|
||||||
"user": "",
|
|
||||||
"password": "",
|
|
||||||
"database": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"kafka": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"brokers": null,
|
|
||||||
"topic": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"webhook": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"endpoint": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
// diff from config1 - amqp.Enable is True
|
|
||||||
config2 = []byte(`{
|
|
||||||
"version": "13",
|
|
||||||
"credential": {
|
|
||||||
"accessKey": "minio",
|
|
||||||
"secretKey": "minio123"
|
|
||||||
},
|
|
||||||
"region": "us-east-1",
|
|
||||||
"logger": {
|
|
||||||
"console": {
|
|
||||||
"enable": true,
|
|
||||||
"level": "debug"
|
|
||||||
},
|
|
||||||
"file": {
|
|
||||||
"enable": false,
|
|
||||||
"fileName": "",
|
|
||||||
"level": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notify": {
|
|
||||||
"amqp": {
|
|
||||||
"1": {
|
|
||||||
"enable": true,
|
|
||||||
"url": "",
|
|
||||||
"exchange": "",
|
|
||||||
"routingKey": "",
|
|
||||||
"exchangeType": "",
|
|
||||||
"mandatory": false,
|
|
||||||
"immediate": false,
|
|
||||||
"durable": false,
|
|
||||||
"internal": false,
|
|
||||||
"noWait": false,
|
|
||||||
"autoDeleted": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nats": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"address": "",
|
|
||||||
"subject": "",
|
|
||||||
"username": "",
|
|
||||||
"password": "",
|
|
||||||
"token": "",
|
|
||||||
"secure": false,
|
|
||||||
"pingInterval": 0,
|
|
||||||
"streaming": {
|
|
||||||
"enable": false,
|
|
||||||
"clusterID": "",
|
|
||||||
"clientID": "",
|
|
||||||
"async": false,
|
|
||||||
"maxPubAcksInflight": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"elasticsearch": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"url": "",
|
|
||||||
"index": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"redis": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"address": "",
|
|
||||||
"password": "",
|
|
||||||
"key": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"postgresql": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"connectionString": "",
|
|
||||||
"table": "",
|
|
||||||
"host": "",
|
|
||||||
"port": "",
|
|
||||||
"user": "",
|
|
||||||
"password": "",
|
|
||||||
"database": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"kafka": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"brokers": null,
|
|
||||||
"topic": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"webhook": {
|
|
||||||
"1": {
|
|
||||||
"enable": false,
|
|
||||||
"endpoint": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestGetValidServerConfig - test for getValidServerConfig.
|
|
||||||
func TestGetValidServerConfig(t *testing.T) {
|
|
||||||
var c1, c2 serverConfig
|
|
||||||
err := json.Unmarshal(config1, &c1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("json unmarshal of %s failed: %v", string(config1), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(config2, &c2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("json unmarshal of %s failed: %v", string(config2), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid config.
|
|
||||||
noErrs := []error{nil, nil, nil, nil}
|
|
||||||
serverConfigs := []serverConfig{c1, c2, c1, c1}
|
|
||||||
validConfig, err := getValidServerConfig(serverConfigs, noErrs)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected a valid config but received %v instead", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(validConfig, c1) {
|
|
||||||
t.Errorf("Expected valid config to be %v but received %v", config1, validConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid config - no quorum.
|
|
||||||
serverConfigs = []serverConfig{c1, c2, c2, c1}
|
|
||||||
_, err = getValidServerConfig(serverConfigs, noErrs)
|
|
||||||
if err != errXLWriteQuorum {
|
|
||||||
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// All errors
|
|
||||||
allErrs := []error{errDiskNotFound, errDiskNotFound, errDiskNotFound, errDiskNotFound}
|
|
||||||
serverConfigs = []serverConfig{{}, {}, {}, {}}
|
|
||||||
_, err = getValidServerConfig(serverConfigs, allErrs)
|
|
||||||
if err != errXLWriteQuorum {
|
|
||||||
t.Errorf("Expected to fail due to lack of quorum but received %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,6 +25,87 @@ const (
|
|||||||
responseRequestIDKey = "x-amz-request-id"
|
responseRequestIDKey = "x-amz-request-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CSVFileHeaderInfo -Can be either USE IGNORE OR NONE, defines what to do with
|
||||||
|
// the first row
|
||||||
|
type CSVFileHeaderInfo string
|
||||||
|
|
||||||
|
// Constants for file header info.
|
||||||
|
const (
|
||||||
|
CSVFileHeaderInfoNone CSVFileHeaderInfo = "NONE"
|
||||||
|
CSVFileHeaderInfoIgnore = "IGNORE"
|
||||||
|
CSVFileHeaderInfoUse = "USE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SelectCompressionType - ONLY GZIP is supported
|
||||||
|
type SelectCompressionType string
|
||||||
|
|
||||||
|
// Constants for compression types under select API.
|
||||||
|
const (
|
||||||
|
SelectCompressionNONE SelectCompressionType = "NONE"
|
||||||
|
SelectCompressionGZIP = "GZIP"
|
||||||
|
SelectCompressionBZIP = "BZIP2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CSVQuoteFields - Can be either Always or AsNeeded
|
||||||
|
type CSVQuoteFields string
|
||||||
|
|
||||||
|
// Constants for csv quote styles.
|
||||||
|
const (
|
||||||
|
CSVQuoteFieldsAlways CSVQuoteFields = "Always"
|
||||||
|
CSVQuoteFieldsAsNeeded = "AsNeeded"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueryExpressionType - Currently can only be SQL
|
||||||
|
type QueryExpressionType string
|
||||||
|
|
||||||
|
// Constants for expression type.
|
||||||
|
const (
|
||||||
|
QueryExpressionTypeSQL QueryExpressionType = "SQL"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONType determines json input serialization type.
|
||||||
|
type JSONType string
|
||||||
|
|
||||||
|
// Constants for JSONTypes.
|
||||||
|
const (
|
||||||
|
JSONDocumentType JSONType = "Document"
|
||||||
|
JSONStreamType = "Stream"
|
||||||
|
JSONLinesType = "Lines"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObjectSelectRequest - represents the input select body
|
||||||
|
type ObjectSelectRequest struct {
|
||||||
|
XMLName xml.Name `xml:"SelectObjectContentRequest" json:"-"`
|
||||||
|
Expression string
|
||||||
|
ExpressionType QueryExpressionType
|
||||||
|
InputSerialization struct {
|
||||||
|
CompressionType SelectCompressionType
|
||||||
|
CSV *struct {
|
||||||
|
FileHeaderInfo CSVFileHeaderInfo
|
||||||
|
RecordDelimiter string
|
||||||
|
FieldDelimiter string
|
||||||
|
QuoteCharacter string
|
||||||
|
QuoteEscapeCharacter string
|
||||||
|
Comments string
|
||||||
|
}
|
||||||
|
JSON *struct {
|
||||||
|
Type JSONType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutputSerialization struct {
|
||||||
|
CSV *struct {
|
||||||
|
QuoteFields CSVQuoteFields
|
||||||
|
RecordDelimiter string
|
||||||
|
FieldDelimiter string
|
||||||
|
QuoteCharacter string
|
||||||
|
QuoteEscapeCharacter string
|
||||||
|
}
|
||||||
|
JSON *struct {
|
||||||
|
RecordDelimiter string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ObjectIdentifier carries key name for the object to delete.
|
// ObjectIdentifier carries key name for the object to delete.
|
||||||
type ObjectIdentifier struct {
|
type ObjectIdentifier struct {
|
||||||
ObjectName string `xml:"Key"`
|
ObjectName string `xml:"Key"`
|
||||||
|
|||||||
+730
-24
@@ -22,10 +22,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/coreos/etcd/client"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
"github.com/minio/minio/pkg/dns"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
"github.com/minio/minio/pkg/s3select"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIError structure
|
// APIError structure
|
||||||
@@ -40,8 +42,8 @@ type APIErrorResponse struct {
|
|||||||
XMLName xml.Name `xml:"Error" json:"-"`
|
XMLName xml.Name `xml:"Error" json:"-"`
|
||||||
Code string
|
Code string
|
||||||
Message string
|
Message string
|
||||||
Key string
|
Key string `xml:"Key,omitempty" json:"Key,omitempty"`
|
||||||
BucketName string
|
BucketName string `xml:"BucketName,omitempty" json:"BucketName,omitempty"`
|
||||||
Resource string
|
Resource string
|
||||||
RequestID string `xml:"RequestId" json:"RequestId"`
|
RequestID string `xml:"RequestId" json:"RequestId"`
|
||||||
HostID string `xml:"HostId" json:"HostId"`
|
HostID string `xml:"HostId" json:"HostId"`
|
||||||
@@ -129,6 +131,9 @@ const (
|
|||||||
ErrInvalidPrefixMarker
|
ErrInvalidPrefixMarker
|
||||||
// Add new error codes here.
|
// Add new error codes here.
|
||||||
|
|
||||||
|
// SSE-S3 related API errors
|
||||||
|
ErrInvalidEncryptionMethod
|
||||||
|
|
||||||
// Server-Side-Encryption (with Customer provided key) related API errors.
|
// Server-Side-Encryption (with Customer provided key) related API errors.
|
||||||
ErrInsecureSSECustomerRequest
|
ErrInsecureSSECustomerRequest
|
||||||
ErrSSEMultipartEncrypted
|
ErrSSEMultipartEncrypted
|
||||||
@@ -140,6 +145,9 @@ const (
|
|||||||
ErrMissingSSECustomerKeyMD5
|
ErrMissingSSECustomerKeyMD5
|
||||||
ErrSSECustomerKeyMD5Mismatch
|
ErrSSECustomerKeyMD5Mismatch
|
||||||
ErrInvalidSSECustomerParameters
|
ErrInvalidSSECustomerParameters
|
||||||
|
ErrIncompatibleEncryptionMethod
|
||||||
|
ErrKMSNotConfigured
|
||||||
|
ErrKMSAuthFailure
|
||||||
|
|
||||||
// Bucket notification related errors.
|
// Bucket notification related errors.
|
||||||
ErrEventNotification
|
ErrEventNotification
|
||||||
@@ -169,7 +177,6 @@ const (
|
|||||||
ErrInvalidResourceName
|
ErrInvalidResourceName
|
||||||
ErrServerNotInitialized
|
ErrServerNotInitialized
|
||||||
ErrOperationTimedOut
|
ErrOperationTimedOut
|
||||||
ErrPartsSizeUnequal
|
|
||||||
ErrInvalidRequest
|
ErrInvalidRequest
|
||||||
// Minio storage class error codes
|
// Minio storage class error codes
|
||||||
ErrInvalidStorageClass
|
ErrInvalidStorageClass
|
||||||
@@ -187,6 +194,7 @@ const (
|
|||||||
ErrAdminCredentialsMismatch
|
ErrAdminCredentialsMismatch
|
||||||
ErrInsecureClientRequest
|
ErrInsecureClientRequest
|
||||||
ErrObjectTampered
|
ErrObjectTampered
|
||||||
|
|
||||||
ErrHealNotImplemented
|
ErrHealNotImplemented
|
||||||
ErrHealNoSuchProcess
|
ErrHealNoSuchProcess
|
||||||
ErrHealInvalidClientToken
|
ErrHealInvalidClientToken
|
||||||
@@ -194,6 +202,93 @@ const (
|
|||||||
ErrHealAlreadyRunning
|
ErrHealAlreadyRunning
|
||||||
ErrHealOverlappingPaths
|
ErrHealOverlappingPaths
|
||||||
ErrIncorrectContinuationToken
|
ErrIncorrectContinuationToken
|
||||||
|
|
||||||
|
//S3 Select Errors
|
||||||
|
ErrEmptyRequestBody
|
||||||
|
ErrUnsupportedFunction
|
||||||
|
ErrInvalidExpressionType
|
||||||
|
ErrBusy
|
||||||
|
ErrUnauthorizedAccess
|
||||||
|
ErrExpressionTooLong
|
||||||
|
ErrIllegalSQLFunctionArgument
|
||||||
|
ErrInvalidKeyPath
|
||||||
|
ErrInvalidCompressionFormat
|
||||||
|
ErrInvalidFileHeaderInfo
|
||||||
|
ErrInvalidJSONType
|
||||||
|
ErrInvalidQuoteFields
|
||||||
|
ErrInvalidRequestParameter
|
||||||
|
ErrInvalidDataType
|
||||||
|
ErrInvalidTextEncoding
|
||||||
|
ErrInvalidDataSource
|
||||||
|
ErrInvalidTableAlias
|
||||||
|
ErrMissingRequiredParameter
|
||||||
|
ErrObjectSerializationConflict
|
||||||
|
ErrUnsupportedSQLOperation
|
||||||
|
ErrUnsupportedSQLStructure
|
||||||
|
ErrUnsupportedSyntax
|
||||||
|
ErrUnsupportedRangeHeader
|
||||||
|
ErrLexerInvalidChar
|
||||||
|
ErrLexerInvalidOperator
|
||||||
|
ErrLexerInvalidLiteral
|
||||||
|
ErrLexerInvalidIONLiteral
|
||||||
|
ErrParseExpectedDatePart
|
||||||
|
ErrParseExpectedKeyword
|
||||||
|
ErrParseExpectedTokenType
|
||||||
|
ErrParseExpected2TokenTypes
|
||||||
|
ErrParseExpectedNumber
|
||||||
|
ErrParseExpectedRightParenBuiltinFunctionCall
|
||||||
|
ErrParseExpectedTypeName
|
||||||
|
ErrParseExpectedWhenClause
|
||||||
|
ErrParseUnsupportedToken
|
||||||
|
ErrParseUnsupportedLiteralsGroupBy
|
||||||
|
ErrParseExpectedMember
|
||||||
|
ErrParseUnsupportedSelect
|
||||||
|
ErrParseUnsupportedCase
|
||||||
|
ErrParseUnsupportedCaseClause
|
||||||
|
ErrParseUnsupportedAlias
|
||||||
|
ErrParseUnsupportedSyntax
|
||||||
|
ErrParseUnknownOperator
|
||||||
|
ErrParseInvalidPathComponent
|
||||||
|
ErrParseMissingIdentAfterAt
|
||||||
|
ErrParseUnexpectedOperator
|
||||||
|
ErrParseUnexpectedTerm
|
||||||
|
ErrParseUnexpectedToken
|
||||||
|
ErrParseUnexpectedKeyword
|
||||||
|
ErrParseExpectedExpression
|
||||||
|
ErrParseExpectedLeftParenAfterCast
|
||||||
|
ErrParseExpectedLeftParenValueConstructor
|
||||||
|
ErrParseExpectedLeftParenBuiltinFunctionCall
|
||||||
|
ErrParseExpectedArgumentDelimiter
|
||||||
|
ErrParseCastArity
|
||||||
|
ErrParseInvalidTypeParam
|
||||||
|
ErrParseEmptySelect
|
||||||
|
ErrParseSelectMissingFrom
|
||||||
|
ErrParseExpectedIdentForGroupName
|
||||||
|
ErrParseExpectedIdentForAlias
|
||||||
|
ErrParseUnsupportedCallWithStar
|
||||||
|
ErrParseNonUnaryAgregateFunctionCall
|
||||||
|
ErrParseMalformedJoin
|
||||||
|
ErrParseExpectedIdentForAt
|
||||||
|
ErrParseAsteriskIsNotAloneInSelectList
|
||||||
|
ErrParseCannotMixSqbAndWildcardInSelectList
|
||||||
|
ErrParseInvalidContextForWildcardInSelectList
|
||||||
|
ErrIncorrectSQLFunctionArgumentType
|
||||||
|
ErrValueParseFailure
|
||||||
|
ErrEvaluatorInvalidArguments
|
||||||
|
ErrIntegerOverflow
|
||||||
|
ErrLikeInvalidInputs
|
||||||
|
ErrCastFailed
|
||||||
|
ErrInvalidCast
|
||||||
|
ErrEvaluatorInvalidTimestampFormatPattern
|
||||||
|
ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing
|
||||||
|
ErrEvaluatorTimestampFormatPatternDuplicateFields
|
||||||
|
ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch
|
||||||
|
ErrEvaluatorUnterminatedTimestampFormatPatternToken
|
||||||
|
ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||||
|
ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||||
|
ErrEvaluatorBindingDoesNotExist
|
||||||
|
ErrInvalidColumnIndex
|
||||||
|
ErrMissingHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
// error code to APIError structure, these fields carry respective
|
// error code to APIError structure, these fields carry respective
|
||||||
@@ -630,6 +725,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrInvalidEncryptionMethod: {
|
||||||
|
Code: "InvalidRequest",
|
||||||
|
Description: "The encryption method specified is not supported",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrInsecureSSECustomerRequest: {
|
ErrInsecureSSECustomerRequest: {
|
||||||
Code: "InvalidRequest",
|
Code: "InvalidRequest",
|
||||||
Description: "Requests specifying Server Side Encryption with Customer provided keys must be made over a secure connection.",
|
Description: "Requests specifying Server Side Encryption with Customer provided keys must be made over a secure connection.",
|
||||||
@@ -680,6 +780,21 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "The provided encryption parameters did not match the ones used originally.",
|
Description: "The provided encryption parameters did not match the ones used originally.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrIncompatibleEncryptionMethod: {
|
||||||
|
Code: "InvalidArgument",
|
||||||
|
Description: "Server side encryption specified with both SSE-C and SSE-S3 headers",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrKMSNotConfigured: {
|
||||||
|
Code: "InvalidArgument",
|
||||||
|
Description: "Server side encryption specified but KMS is not configured",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrKMSAuthFailure: {
|
||||||
|
Code: "InvalidArgument",
|
||||||
|
Description: "Server side encryption specified but KMS authorization failed",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
|
||||||
/// S3 extensions.
|
/// S3 extensions.
|
||||||
ErrContentSHA256Mismatch: {
|
ErrContentSHA256Mismatch: {
|
||||||
@@ -692,7 +807,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
ErrStorageFull: {
|
ErrStorageFull: {
|
||||||
Code: "XMinioStorageFull",
|
Code: "XMinioStorageFull",
|
||||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
|
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
|
||||||
HTTPStatusCode: http.StatusInternalServerError,
|
HTTPStatusCode: http.StatusInsufficientStorage,
|
||||||
},
|
},
|
||||||
ErrRequestBodyParse: {
|
ErrRequestBodyParse: {
|
||||||
Code: "XMinioRequestBodyParse",
|
Code: "XMinioRequestBodyParse",
|
||||||
@@ -785,11 +900,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "Your metadata headers are not supported.",
|
Description: "Your metadata headers are not supported.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrPartsSizeUnequal: {
|
|
||||||
Code: "XMinioPartsSizeUnequal",
|
|
||||||
Description: "All parts except the last part should be of the same size.",
|
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
ErrObjectTampered: {
|
ErrObjectTampered: {
|
||||||
Code: "XMinioObjectTampered",
|
Code: "XMinioObjectTampered",
|
||||||
Description: errObjectTampered.Error(),
|
Description: errObjectTampered.Error(),
|
||||||
@@ -800,6 +910,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Generic Invalid-Request error. Should be used for response errors only for unlikely
|
// Generic Invalid-Request error. Should be used for response errors only for unlikely
|
||||||
// corner case errors for which introducing new APIErrorCode is not worth it. LogIf()
|
// corner case errors for which introducing new APIErrorCode is not worth it. LogIf()
|
||||||
// should be used to log the error at the source of the error for debugging purposes.
|
// should be used to log the error at the source of the error for debugging purposes.
|
||||||
@@ -848,6 +959,432 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
|||||||
Description: "The continuation token provided is incorrect",
|
Description: "The continuation token provided is incorrect",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
//S3 Select API Errors
|
||||||
|
ErrEmptyRequestBody: {
|
||||||
|
Code: "EmptyRequestBody",
|
||||||
|
Description: "Request body cannot be empty.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrUnsupportedFunction: {
|
||||||
|
Code: "UnsupportedFunction",
|
||||||
|
Description: "Encountered an unsupported SQL function.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidDataSource: {
|
||||||
|
Code: "InvalidDataSource",
|
||||||
|
Description: "Invalid data source type. Only CSV and JSON are supported at this time.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidExpressionType: {
|
||||||
|
Code: "InvalidExpressionType",
|
||||||
|
Description: "The ExpressionType is invalid. Only SQL expressions are supported at this time.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrBusy: {
|
||||||
|
Code: "Busy",
|
||||||
|
Description: "The service is unavailable. Please retry.",
|
||||||
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
ErrUnauthorizedAccess: {
|
||||||
|
Code: "UnauthorizedAccess",
|
||||||
|
Description: "You are not authorized to perform this operation",
|
||||||
|
HTTPStatusCode: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
ErrExpressionTooLong: {
|
||||||
|
Code: "ExpressionTooLong",
|
||||||
|
Description: "The SQL expression is too long: The maximum byte-length for the SQL expression is 256 KB.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrIllegalSQLFunctionArgument: {
|
||||||
|
Code: "IllegalSqlFunctionArgument",
|
||||||
|
Description: "Illegal argument was used in the SQL function.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidKeyPath: {
|
||||||
|
Code: "InvalidKeyPath",
|
||||||
|
Description: "Key path in the SQL expression is invalid.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidCompressionFormat: {
|
||||||
|
Code: "InvalidCompressionFormat",
|
||||||
|
Description: "The file is not in a supported compression format. Only GZIP is supported at this time.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidFileHeaderInfo: {
|
||||||
|
Code: "InvalidFileHeaderInfo",
|
||||||
|
Description: "The FileHeaderInfo is invalid. Only NONE, USE, and IGNORE are supported.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidJSONType: {
|
||||||
|
Code: "InvalidJsonType",
|
||||||
|
Description: "The JsonType is invalid. Only DOCUMENT and LINES are supported at this time.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidQuoteFields: {
|
||||||
|
Code: "InvalidQuoteFields",
|
||||||
|
Description: "The QuoteFields is invalid. Only ALWAYS and ASNEEDED are supported.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidRequestParameter: {
|
||||||
|
Code: "InvalidRequestParameter",
|
||||||
|
Description: "The value of a parameter in SelectRequest element is invalid. Check the service API documentation and try again.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidDataType: {
|
||||||
|
Code: "InvalidDataType",
|
||||||
|
Description: "The SQL expression contains an invalid data type.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidTextEncoding: {
|
||||||
|
Code: "InvalidTextEncoding",
|
||||||
|
Description: "Invalid encoding type. Only UTF-8 encoding is supported at this time.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidTableAlias: {
|
||||||
|
Code: "InvalidTableAlias",
|
||||||
|
Description: "The SQL expression contains an invalid table alias.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrMissingRequiredParameter: {
|
||||||
|
Code: "MissingRequiredParameter",
|
||||||
|
Description: "The SelectRequest entity is missing a required parameter. Check the service documentation and try again.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrObjectSerializationConflict: {
|
||||||
|
Code: "ObjectSerializationConflict",
|
||||||
|
Description: "The SelectRequest entity can only contain one of CSV or JSON. Check the service documentation and try again.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrUnsupportedSQLOperation: {
|
||||||
|
Code: "UnsupportedSqlOperation",
|
||||||
|
Description: "Encountered an unsupported SQL operation.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrUnsupportedSQLStructure: {
|
||||||
|
Code: "UnsupportedSqlStructure",
|
||||||
|
Description: "Encountered an unsupported SQL structure. Check the SQL Reference.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrUnsupportedSyntax: {
|
||||||
|
Code: "UnsupportedSyntax",
|
||||||
|
Description: "Encountered invalid syntax.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrUnsupportedRangeHeader: {
|
||||||
|
Code: "UnsupportedRangeHeader",
|
||||||
|
Description: "Range header is not supported for this operation.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrLexerInvalidChar: {
|
||||||
|
Code: "LexerInvalidChar",
|
||||||
|
Description: "The SQL expression contains an invalid character.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrLexerInvalidOperator: {
|
||||||
|
Code: "LexerInvalidOperator",
|
||||||
|
Description: "The SQL expression contains an invalid literal.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrLexerInvalidLiteral: {
|
||||||
|
Code: "LexerInvalidLiteral",
|
||||||
|
Description: "The SQL expression contains an invalid operator.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrLexerInvalidIONLiteral: {
|
||||||
|
Code: "LexerInvalidIONLiteral",
|
||||||
|
Description: "The SQL expression contains an invalid operator.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedDatePart: {
|
||||||
|
Code: "ParseExpectedDatePart",
|
||||||
|
Description: "Did not find the expected date part in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedKeyword: {
|
||||||
|
Code: "ParseExpectedKeyword",
|
||||||
|
Description: "Did not find the expected keyword in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedTokenType: {
|
||||||
|
Code: "ParseExpectedTokenType",
|
||||||
|
Description: "Did not find the expected token in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpected2TokenTypes: {
|
||||||
|
Code: "ParseExpected2TokenTypes",
|
||||||
|
Description: "Did not find the expected token in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedNumber: {
|
||||||
|
Code: "ParseExpectedNumber",
|
||||||
|
Description: "Did not find the expected number in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedRightParenBuiltinFunctionCall: {
|
||||||
|
Code: "ParseExpectedRightParenBuiltinFunctionCall",
|
||||||
|
Description: "Did not find the expected right parenthesis character in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedTypeName: {
|
||||||
|
Code: "ParseExpectedTypeName",
|
||||||
|
Description: "Did not find the expected type name in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedWhenClause: {
|
||||||
|
Code: "ParseExpectedWhenClause",
|
||||||
|
Description: "Did not find the expected WHEN clause in the SQL expression. CASE is not supported.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnsupportedToken: {
|
||||||
|
Code: "ParseUnsupportedToken",
|
||||||
|
Description: "The SQL expression contains an unsupported token.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnsupportedLiteralsGroupBy: {
|
||||||
|
Code: "ParseUnsupportedLiteralsGroupBy",
|
||||||
|
Description: "The SQL expression contains an unsupported use of GROUP BY.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedMember: {
|
||||||
|
Code: "ParseExpectedMember",
|
||||||
|
Description: "The SQL expression contains an unsupported use of MEMBER.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnsupportedSelect: {
|
||||||
|
Code: "ParseUnsupportedSelect",
|
||||||
|
Description: "The SQL expression contains an unsupported use of SELECT.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnsupportedCase: {
|
||||||
|
Code: "ParseUnsupportedCase",
|
||||||
|
Description: "The SQL expression contains an unsupported use of CASE.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnsupportedCaseClause: {
|
||||||
|
Code: "ParseUnsupportedCaseClause",
|
||||||
|
Description: "The SQL expression contains an unsupported use of CASE.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnsupportedAlias: {
|
||||||
|
Code: "ParseUnsupportedAlias",
|
||||||
|
Description: "The SQL expression contains an unsupported use of ALIAS.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnsupportedSyntax: {
|
||||||
|
Code: "ParseUnsupportedSyntax",
|
||||||
|
Description: "The SQL expression contains unsupported syntax.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnknownOperator: {
|
||||||
|
Code: "ParseUnknownOperator",
|
||||||
|
Description: "The SQL expression contains an invalid operator.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseInvalidPathComponent: {
|
||||||
|
Code: "ParseInvalidPathComponent",
|
||||||
|
Description: "The SQL expression contains an invalid path component.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseMissingIdentAfterAt: {
|
||||||
|
Code: "ParseMissingIdentAfterAt",
|
||||||
|
Description: "Did not find the expected identifier after the @ symbol in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnexpectedOperator: {
|
||||||
|
Code: "ParseUnexpectedOperator",
|
||||||
|
Description: "The SQL expression contains an unexpected operator.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnexpectedTerm: {
|
||||||
|
Code: "ParseUnexpectedTerm",
|
||||||
|
Description: "The SQL expression contains an unexpected term.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnexpectedToken: {
|
||||||
|
Code: "ParseUnexpectedToken",
|
||||||
|
Description: "The SQL expression contains an unexpected token.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnexpectedKeyword: {
|
||||||
|
Code: "ParseUnexpectedKeyword",
|
||||||
|
Description: "The SQL expression contains an unexpected keyword.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedExpression: {
|
||||||
|
Code: "ParseExpectedExpression",
|
||||||
|
Description: "Did not find the expected SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedLeftParenAfterCast: {
|
||||||
|
Code: "ParseExpectedLeftParenAfterCast",
|
||||||
|
Description: "Did not find expected the left parenthesis in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedLeftParenValueConstructor: {
|
||||||
|
Code: "ParseExpectedLeftParenValueConstructor",
|
||||||
|
Description: "Did not find expected the left parenthesis in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedLeftParenBuiltinFunctionCall: {
|
||||||
|
Code: "ParseExpectedLeftParenBuiltinFunctionCall",
|
||||||
|
Description: "Did not find the expected left parenthesis in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedArgumentDelimiter: {
|
||||||
|
Code: "ParseExpectedArgumentDelimiter",
|
||||||
|
Description: "Did not find the expected argument delimiter in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseCastArity: {
|
||||||
|
Code: "ParseCastArity",
|
||||||
|
Description: "The SQL expression CAST has incorrect arity.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseInvalidTypeParam: {
|
||||||
|
Code: "ParseInvalidTypeParam",
|
||||||
|
Description: "The SQL expression contains an invalid parameter value.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseEmptySelect: {
|
||||||
|
Code: "ParseEmptySelect",
|
||||||
|
Description: "The SQL expression contains an empty SELECT.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseSelectMissingFrom: {
|
||||||
|
Code: "ParseSelectMissingFrom",
|
||||||
|
Description: "GROUP is not supported in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedIdentForGroupName: {
|
||||||
|
Code: "ParseExpectedIdentForGroupName",
|
||||||
|
Description: "GROUP is not supported in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedIdentForAlias: {
|
||||||
|
Code: "ParseExpectedIdentForAlias",
|
||||||
|
Description: "Did not find the expected identifier for the alias in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseUnsupportedCallWithStar: {
|
||||||
|
Code: "ParseUnsupportedCallWithStar",
|
||||||
|
Description: "Only COUNT with (*) as a parameter is supported in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseNonUnaryAgregateFunctionCall: {
|
||||||
|
Code: "ParseNonUnaryAgregateFunctionCall",
|
||||||
|
Description: "Only one argument is supported for aggregate functions in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseMalformedJoin: {
|
||||||
|
Code: "ParseMalformedJoin",
|
||||||
|
Description: "JOIN is not supported in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseExpectedIdentForAt: {
|
||||||
|
Code: "ParseExpectedIdentForAt",
|
||||||
|
Description: "Did not find the expected identifier for AT name in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseAsteriskIsNotAloneInSelectList: {
|
||||||
|
Code: "ParseAsteriskIsNotAloneInSelectList",
|
||||||
|
Description: "Other expressions are not allowed in the SELECT list when '*' is used without dot notation in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseCannotMixSqbAndWildcardInSelectList: {
|
||||||
|
Code: "ParseCannotMixSqbAndWildcardInSelectList",
|
||||||
|
Description: "Cannot mix [] and * in the same expression in a SELECT list in SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrParseInvalidContextForWildcardInSelectList: {
|
||||||
|
Code: "ParseInvalidContextForWildcardInSelectList",
|
||||||
|
Description: "Invalid use of * in SELECT list in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrIncorrectSQLFunctionArgumentType: {
|
||||||
|
Code: "IncorrectSqlFunctionArgumentType",
|
||||||
|
Description: "Incorrect type of arguments in function call in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrValueParseFailure: {
|
||||||
|
Code: "ValueParseFailure",
|
||||||
|
Description: "Time stamp parse failure in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrEvaluatorInvalidArguments: {
|
||||||
|
Code: "EvaluatorInvalidArguments",
|
||||||
|
Description: "Incorrect number of arguments in the function call in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrIntegerOverflow: {
|
||||||
|
Code: "IntegerOverflow",
|
||||||
|
Description: "Int overflow or underflow in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrLikeInvalidInputs: {
|
||||||
|
Code: "LikeInvalidInputs",
|
||||||
|
Description: "Invalid argument given to the LIKE clause in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrCastFailed: {
|
||||||
|
Code: "CastFailed",
|
||||||
|
Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidCast: {
|
||||||
|
Code: "InvalidCast",
|
||||||
|
Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrEvaluatorInvalidTimestampFormatPattern: {
|
||||||
|
Code: "EvaluatorInvalidTimestampFormatPattern",
|
||||||
|
Description: "Time stamp format pattern requires additional fields in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing: {
|
||||||
|
Code: "EvaluatorInvalidTimestampFormatPatternSymbolForParsing",
|
||||||
|
Description: "Time stamp format pattern contains a valid format symbol that cannot be applied to time stamp parsing in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrEvaluatorTimestampFormatPatternDuplicateFields: {
|
||||||
|
Code: "EvaluatorTimestampFormatPatternDuplicateFields",
|
||||||
|
Description: "Time stamp format pattern contains multiple format specifiers representing the time stamp field in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch: {
|
||||||
|
Code: "EvaluatorUnterminatedTimestampFormatPatternToken",
|
||||||
|
Description: "Time stamp format pattern contains unterminated token in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrEvaluatorUnterminatedTimestampFormatPatternToken: {
|
||||||
|
Code: "EvaluatorInvalidTimestampFormatPatternToken",
|
||||||
|
Description: "Time stamp format pattern contains an invalid token in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrEvaluatorInvalidTimestampFormatPatternToken: {
|
||||||
|
Code: "EvaluatorInvalidTimestampFormatPatternToken",
|
||||||
|
Description: "Time stamp format pattern contains an invalid token in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrEvaluatorInvalidTimestampFormatPatternSymbol: {
|
||||||
|
Code: "EvaluatorInvalidTimestampFormatPatternSymbol",
|
||||||
|
Description: "Time stamp format pattern contains an invalid symbol in the SQL expression.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrInvalidColumnIndex: {
|
||||||
|
Code: "InvalidColumnIndex",
|
||||||
|
Description: "Column index in the SQL expression is invalid.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrEvaluatorBindingDoesNotExist: {
|
||||||
|
Code: "ErrEvaluatorBindingDoesNotExist",
|
||||||
|
Description: "A column name or a path provided does not exist in the SQL expression",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrMissingHeaders: {
|
||||||
|
Code: "MissingHeaders",
|
||||||
|
Description: "Some headers in the query are missing from the file. Check the file and try again.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
// Add your error structure here.
|
// Add your error structure here.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,17 +1409,19 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
case auth.ErrInvalidSecretKeyLength:
|
case auth.ErrInvalidSecretKeyLength:
|
||||||
apiErr = ErrAdminInvalidSecretKey
|
apiErr = ErrAdminInvalidSecretKey
|
||||||
// SSE errors
|
// SSE errors
|
||||||
|
case crypto.ErrInvalidEncryptionMethod:
|
||||||
|
apiErr = ErrInvalidEncryptionMethod
|
||||||
case errInsecureSSERequest:
|
case errInsecureSSERequest:
|
||||||
apiErr = ErrInsecureSSECustomerRequest
|
apiErr = ErrInsecureSSECustomerRequest
|
||||||
case errInvalidSSEAlgorithm:
|
case crypto.ErrInvalidCustomerAlgorithm:
|
||||||
apiErr = ErrInvalidSSECustomerAlgorithm
|
apiErr = ErrInvalidSSECustomerAlgorithm
|
||||||
case errInvalidSSEKey:
|
case crypto.ErrInvalidCustomerKey:
|
||||||
apiErr = ErrInvalidSSECustomerKey
|
apiErr = ErrInvalidSSECustomerKey
|
||||||
case errMissingSSEKey:
|
case crypto.ErrMissingCustomerKey:
|
||||||
apiErr = ErrMissingSSECustomerKey
|
apiErr = ErrMissingSSECustomerKey
|
||||||
case errMissingSSEKeyMD5:
|
case crypto.ErrMissingCustomerKeyMD5:
|
||||||
apiErr = ErrMissingSSECustomerKeyMD5
|
apiErr = ErrMissingSSECustomerKeyMD5
|
||||||
case errSSEKeyMD5Mismatch:
|
case crypto.ErrCustomerKeyMD5Mismatch:
|
||||||
apiErr = ErrSSECustomerKeyMD5Mismatch
|
apiErr = ErrSSECustomerKeyMD5Mismatch
|
||||||
case errObjectTampered:
|
case errObjectTampered:
|
||||||
apiErr = ErrObjectTampered
|
apiErr = ErrObjectTampered
|
||||||
@@ -890,11 +1429,180 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrSSEEncryptedObject
|
apiErr = ErrSSEEncryptedObject
|
||||||
case errInvalidSSEParameters:
|
case errInvalidSSEParameters:
|
||||||
apiErr = ErrInvalidSSECustomerParameters
|
apiErr = ErrInvalidSSECustomerParameters
|
||||||
case errSSEKeyMismatch:
|
case crypto.ErrInvalidCustomerKey:
|
||||||
apiErr = ErrAccessDenied // no access without correct key
|
apiErr = ErrAccessDenied // no access without correct key
|
||||||
|
case crypto.ErrIncompatibleEncryptionMethod:
|
||||||
|
apiErr = ErrIncompatibleEncryptionMethod
|
||||||
|
case errKMSNotConfigured:
|
||||||
|
apiErr = ErrKMSNotConfigured
|
||||||
|
case crypto.ErrKMSAuthLogin:
|
||||||
|
apiErr = ErrKMSAuthFailure
|
||||||
case context.Canceled, context.DeadlineExceeded:
|
case context.Canceled, context.DeadlineExceeded:
|
||||||
apiErr = ErrOperationTimedOut
|
apiErr = ErrOperationTimedOut
|
||||||
}
|
}
|
||||||
|
switch err {
|
||||||
|
case s3select.ErrBusy:
|
||||||
|
apiErr = ErrBusy
|
||||||
|
case s3select.ErrUnauthorizedAccess:
|
||||||
|
apiErr = ErrUnauthorizedAccess
|
||||||
|
case s3select.ErrExpressionTooLong:
|
||||||
|
apiErr = ErrExpressionTooLong
|
||||||
|
case s3select.ErrIllegalSQLFunctionArgument:
|
||||||
|
apiErr = ErrIllegalSQLFunctionArgument
|
||||||
|
case s3select.ErrInvalidKeyPath:
|
||||||
|
apiErr = ErrInvalidKeyPath
|
||||||
|
case s3select.ErrInvalidCompressionFormat:
|
||||||
|
apiErr = ErrInvalidCompressionFormat
|
||||||
|
case s3select.ErrInvalidFileHeaderInfo:
|
||||||
|
apiErr = ErrInvalidFileHeaderInfo
|
||||||
|
case s3select.ErrInvalidJSONType:
|
||||||
|
apiErr = ErrInvalidJSONType
|
||||||
|
case s3select.ErrInvalidQuoteFields:
|
||||||
|
apiErr = ErrInvalidQuoteFields
|
||||||
|
case s3select.ErrInvalidRequestParameter:
|
||||||
|
apiErr = ErrInvalidRequestParameter
|
||||||
|
case s3select.ErrInvalidDataType:
|
||||||
|
apiErr = ErrInvalidDataType
|
||||||
|
case s3select.ErrInvalidTextEncoding:
|
||||||
|
apiErr = ErrInvalidTextEncoding
|
||||||
|
case s3select.ErrInvalidTableAlias:
|
||||||
|
apiErr = ErrInvalidTableAlias
|
||||||
|
case s3select.ErrMissingRequiredParameter:
|
||||||
|
apiErr = ErrMissingRequiredParameter
|
||||||
|
case s3select.ErrObjectSerializationConflict:
|
||||||
|
apiErr = ErrObjectSerializationConflict
|
||||||
|
case s3select.ErrUnsupportedSQLOperation:
|
||||||
|
apiErr = ErrUnsupportedSQLOperation
|
||||||
|
case s3select.ErrUnsupportedSQLStructure:
|
||||||
|
apiErr = ErrUnsupportedSQLStructure
|
||||||
|
case s3select.ErrUnsupportedSyntax:
|
||||||
|
apiErr = ErrUnsupportedSyntax
|
||||||
|
case s3select.ErrUnsupportedRangeHeader:
|
||||||
|
apiErr = ErrUnsupportedRangeHeader
|
||||||
|
case s3select.ErrLexerInvalidChar:
|
||||||
|
apiErr = ErrLexerInvalidChar
|
||||||
|
case s3select.ErrLexerInvalidOperator:
|
||||||
|
apiErr = ErrLexerInvalidOperator
|
||||||
|
case s3select.ErrLexerInvalidLiteral:
|
||||||
|
apiErr = ErrLexerInvalidLiteral
|
||||||
|
case s3select.ErrLexerInvalidIONLiteral:
|
||||||
|
apiErr = ErrLexerInvalidIONLiteral
|
||||||
|
case s3select.ErrParseExpectedDatePart:
|
||||||
|
apiErr = ErrParseExpectedDatePart
|
||||||
|
case s3select.ErrParseExpectedKeyword:
|
||||||
|
apiErr = ErrParseExpectedKeyword
|
||||||
|
case s3select.ErrParseExpectedTokenType:
|
||||||
|
apiErr = ErrParseExpectedTokenType
|
||||||
|
case s3select.ErrParseExpected2TokenTypes:
|
||||||
|
apiErr = ErrParseExpected2TokenTypes
|
||||||
|
case s3select.ErrParseExpectedNumber:
|
||||||
|
apiErr = ErrParseExpectedNumber
|
||||||
|
case s3select.ErrParseExpectedRightParenBuiltinFunctionCall:
|
||||||
|
apiErr = ErrParseExpectedRightParenBuiltinFunctionCall
|
||||||
|
case s3select.ErrParseExpectedTypeName:
|
||||||
|
apiErr = ErrParseExpectedTypeName
|
||||||
|
case s3select.ErrParseExpectedWhenClause:
|
||||||
|
apiErr = ErrParseExpectedWhenClause
|
||||||
|
case s3select.ErrParseUnsupportedToken:
|
||||||
|
apiErr = ErrParseUnsupportedToken
|
||||||
|
case s3select.ErrParseUnsupportedLiteralsGroupBy:
|
||||||
|
apiErr = ErrParseUnsupportedLiteralsGroupBy
|
||||||
|
case s3select.ErrParseExpectedMember:
|
||||||
|
apiErr = ErrParseExpectedMember
|
||||||
|
case s3select.ErrParseUnsupportedSelect:
|
||||||
|
apiErr = ErrParseUnsupportedSelect
|
||||||
|
case s3select.ErrParseUnsupportedCase:
|
||||||
|
apiErr = ErrParseUnsupportedCase
|
||||||
|
case s3select.ErrParseUnsupportedCaseClause:
|
||||||
|
apiErr = ErrParseUnsupportedCaseClause
|
||||||
|
case s3select.ErrParseUnsupportedAlias:
|
||||||
|
apiErr = ErrParseUnsupportedAlias
|
||||||
|
case s3select.ErrParseUnsupportedSyntax:
|
||||||
|
apiErr = ErrParseUnsupportedSyntax
|
||||||
|
case s3select.ErrParseUnknownOperator:
|
||||||
|
apiErr = ErrParseUnknownOperator
|
||||||
|
case s3select.ErrParseInvalidPathComponent:
|
||||||
|
apiErr = ErrParseInvalidPathComponent
|
||||||
|
case s3select.ErrParseMissingIdentAfterAt:
|
||||||
|
apiErr = ErrParseMissingIdentAfterAt
|
||||||
|
case s3select.ErrParseUnexpectedOperator:
|
||||||
|
apiErr = ErrParseUnexpectedOperator
|
||||||
|
case s3select.ErrParseUnexpectedTerm:
|
||||||
|
apiErr = ErrParseUnexpectedTerm
|
||||||
|
case s3select.ErrParseUnexpectedToken:
|
||||||
|
apiErr = ErrParseUnexpectedToken
|
||||||
|
case s3select.ErrParseUnexpectedKeyword:
|
||||||
|
apiErr = ErrParseUnexpectedKeyword
|
||||||
|
case s3select.ErrParseExpectedExpression:
|
||||||
|
apiErr = ErrParseExpectedExpression
|
||||||
|
case s3select.ErrParseExpectedLeftParenAfterCast:
|
||||||
|
apiErr = ErrParseExpectedLeftParenAfterCast
|
||||||
|
case s3select.ErrParseExpectedLeftParenValueConstructor:
|
||||||
|
apiErr = ErrParseExpectedLeftParenValueConstructor
|
||||||
|
case s3select.ErrParseExpectedLeftParenBuiltinFunctionCall:
|
||||||
|
apiErr = ErrParseExpectedLeftParenBuiltinFunctionCall
|
||||||
|
case s3select.ErrParseExpectedArgumentDelimiter:
|
||||||
|
apiErr = ErrParseExpectedArgumentDelimiter
|
||||||
|
case s3select.ErrParseCastArity:
|
||||||
|
apiErr = ErrParseCastArity
|
||||||
|
case s3select.ErrParseInvalidTypeParam:
|
||||||
|
apiErr = ErrParseInvalidTypeParam
|
||||||
|
case s3select.ErrParseEmptySelect:
|
||||||
|
apiErr = ErrParseEmptySelect
|
||||||
|
case s3select.ErrParseSelectMissingFrom:
|
||||||
|
apiErr = ErrParseSelectMissingFrom
|
||||||
|
case s3select.ErrParseExpectedIdentForGroupName:
|
||||||
|
apiErr = ErrParseExpectedIdentForGroupName
|
||||||
|
case s3select.ErrParseExpectedIdentForAlias:
|
||||||
|
apiErr = ErrParseExpectedIdentForAlias
|
||||||
|
case s3select.ErrParseUnsupportedCallWithStar:
|
||||||
|
apiErr = ErrParseUnsupportedCallWithStar
|
||||||
|
case s3select.ErrParseNonUnaryAgregateFunctionCall:
|
||||||
|
apiErr = ErrParseNonUnaryAgregateFunctionCall
|
||||||
|
case s3select.ErrParseMalformedJoin:
|
||||||
|
apiErr = ErrParseMalformedJoin
|
||||||
|
case s3select.ErrParseExpectedIdentForAt:
|
||||||
|
apiErr = ErrParseExpectedIdentForAt
|
||||||
|
case s3select.ErrParseAsteriskIsNotAloneInSelectList:
|
||||||
|
apiErr = ErrParseAsteriskIsNotAloneInSelectList
|
||||||
|
case s3select.ErrParseCannotMixSqbAndWildcardInSelectList:
|
||||||
|
apiErr = ErrParseCannotMixSqbAndWildcardInSelectList
|
||||||
|
case s3select.ErrParseInvalidContextForWildcardInSelectList:
|
||||||
|
apiErr = ErrParseInvalidContextForWildcardInSelectList
|
||||||
|
case s3select.ErrIncorrectSQLFunctionArgumentType:
|
||||||
|
apiErr = ErrIncorrectSQLFunctionArgumentType
|
||||||
|
case s3select.ErrValueParseFailure:
|
||||||
|
apiErr = ErrValueParseFailure
|
||||||
|
case s3select.ErrIntegerOverflow:
|
||||||
|
apiErr = ErrIntegerOverflow
|
||||||
|
case s3select.ErrLikeInvalidInputs:
|
||||||
|
apiErr = ErrLikeInvalidInputs
|
||||||
|
case s3select.ErrCastFailed:
|
||||||
|
apiErr = ErrCastFailed
|
||||||
|
case s3select.ErrInvalidCast:
|
||||||
|
apiErr = ErrInvalidCast
|
||||||
|
case s3select.ErrEvaluatorInvalidTimestampFormatPattern:
|
||||||
|
apiErr = ErrEvaluatorInvalidTimestampFormatPattern
|
||||||
|
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing:
|
||||||
|
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing
|
||||||
|
case s3select.ErrEvaluatorTimestampFormatPatternDuplicateFields:
|
||||||
|
apiErr = ErrEvaluatorTimestampFormatPatternDuplicateFields
|
||||||
|
case s3select.ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch:
|
||||||
|
apiErr = ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch
|
||||||
|
case s3select.ErrEvaluatorUnterminatedTimestampFormatPatternToken:
|
||||||
|
apiErr = ErrEvaluatorUnterminatedTimestampFormatPatternToken
|
||||||
|
case s3select.ErrEvaluatorInvalidTimestampFormatPatternToken:
|
||||||
|
apiErr = ErrEvaluatorInvalidTimestampFormatPatternToken
|
||||||
|
case s3select.ErrEvaluatorInvalidTimestampFormatPatternSymbol:
|
||||||
|
apiErr = ErrEvaluatorInvalidTimestampFormatPatternSymbol
|
||||||
|
case s3select.ErrInvalidColumnIndex:
|
||||||
|
apiErr = ErrInvalidColumnIndex
|
||||||
|
case s3select.ErrEvaluatorBindingDoesNotExist:
|
||||||
|
apiErr = ErrEvaluatorBindingDoesNotExist
|
||||||
|
case s3select.ErrMissingHeaders:
|
||||||
|
apiErr = ErrMissingHeaders
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if apiErr != ErrNone {
|
if apiErr != ErrNone {
|
||||||
// If there was a match in the above switch case.
|
// If there was a match in the above switch case.
|
||||||
@@ -903,10 +1611,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
|
|
||||||
// etcd specific errors, a key is always a bucket for us return
|
// etcd specific errors, a key is always a bucket for us return
|
||||||
// ErrNoSuchBucket in such a case.
|
// ErrNoSuchBucket in such a case.
|
||||||
if e, ok := err.(*client.Error); ok {
|
if err == dns.ErrNoEntriesFound {
|
||||||
if e.Code == client.ErrorCodeKeyNotFound {
|
return ErrNoSuchBucket
|
||||||
return ErrNoSuchBucket
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
@@ -972,8 +1678,6 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrEntityTooLarge
|
apiErr = ErrEntityTooLarge
|
||||||
case UnsupportedMetadata:
|
case UnsupportedMetadata:
|
||||||
apiErr = ErrUnsupportedMetadata
|
apiErr = ErrUnsupportedMetadata
|
||||||
case PartsSizeUnequal:
|
|
||||||
apiErr = ErrPartsSizeUnequal
|
|
||||||
case BucketPolicyNotFound:
|
case BucketPolicyNotFound:
|
||||||
apiErr = ErrNoSuchBucketPolicy
|
apiErr = ErrNoSuchBucketPolicy
|
||||||
case *event.ErrInvalidEventName:
|
case *event.ErrInvalidEventName:
|
||||||
@@ -1000,6 +1704,8 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrUnsupportedNotification
|
apiErr = ErrUnsupportedNotification
|
||||||
case BackendDown:
|
case BackendDown:
|
||||||
apiErr = ErrBackendDown
|
apiErr = ErrBackendDown
|
||||||
|
case crypto.Error:
|
||||||
|
apiErr = ErrObjectTampered
|
||||||
default:
|
default:
|
||||||
apiErr = ErrInternalError
|
apiErr = ErrInternalError
|
||||||
}
|
}
|
||||||
@@ -1014,12 +1720,12 @@ func getAPIError(code APIErrorCode) APIError {
|
|||||||
|
|
||||||
// getErrorResponse gets in standard error and resource value and
|
// getErrorResponse gets in standard error and resource value and
|
||||||
// provides a encodable populated response values
|
// provides a encodable populated response values
|
||||||
func getAPIErrorResponse(err APIError, resource string) APIErrorResponse {
|
func getAPIErrorResponse(err APIError, resource, requestid string) APIErrorResponse {
|
||||||
return APIErrorResponse{
|
return APIErrorResponse{
|
||||||
Code: err.Code,
|
Code: err.Code,
|
||||||
Message: err.Description,
|
Message: err.Description,
|
||||||
Resource: resource,
|
Resource: resource,
|
||||||
RequestID: "3L137",
|
RequestID: requestid,
|
||||||
HostID: "3L137",
|
HostID: "3L137",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,11 +53,11 @@ var toAPIErrorCodeTests = []struct {
|
|||||||
|
|
||||||
// SSE-C errors
|
// SSE-C errors
|
||||||
{err: errInsecureSSERequest, errCode: ErrInsecureSSECustomerRequest},
|
{err: errInsecureSSERequest, errCode: ErrInsecureSSECustomerRequest},
|
||||||
{err: errInvalidSSEAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
{err: crypto.ErrInvalidCustomerAlgorithm, errCode: ErrInvalidSSECustomerAlgorithm},
|
||||||
{err: errMissingSSEKey, errCode: ErrMissingSSECustomerKey},
|
{err: crypto.ErrMissingCustomerKey, errCode: ErrMissingSSECustomerKey},
|
||||||
{err: errInvalidSSEKey, errCode: ErrInvalidSSECustomerKey},
|
{err: crypto.ErrInvalidCustomerKey, errCode: ErrInvalidSSECustomerKey},
|
||||||
{err: errMissingSSEKeyMD5, errCode: ErrMissingSSECustomerKeyMD5},
|
{err: crypto.ErrMissingCustomerKeyMD5, errCode: ErrMissingSSECustomerKeyMD5},
|
||||||
{err: errSSEKeyMD5Mismatch, errCode: ErrSSECustomerKeyMD5Mismatch},
|
{err: crypto.ErrCustomerKeyMD5Mismatch, errCode: ErrSSECustomerKeyMD5Mismatch},
|
||||||
{err: errObjectTampered, errCode: ErrObjectTampered},
|
{err: errObjectTampered, errCode: ErrObjectTampered},
|
||||||
|
|
||||||
{err: nil, errCode: ErrNone},
|
{err: nil, errCode: ErrNone},
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ func mustGetRequestID(t time.Time) string {
|
|||||||
|
|
||||||
// Write http common headers
|
// Write http common headers
|
||||||
func setCommonHeaders(w http.ResponseWriter) {
|
func setCommonHeaders(w http.ResponseWriter) {
|
||||||
// Set unique request ID for each reply.
|
|
||||||
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
|
|
||||||
w.Header().Set("Server", globalServerUserAgent)
|
w.Header().Set("Server", globalServerUserAgent)
|
||||||
// Set `x-amz-bucket-region` only if region is set on the server
|
// Set `x-amz-bucket-region` only if region is set on the server
|
||||||
// by default minio uses an empty region.
|
// by default minio uses an empty region.
|
||||||
@@ -102,6 +100,5 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, contentRange *h
|
|||||||
// Override content-length
|
// Override content-length
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.getLength(), 10))
|
w.Header().Set("Content-Length", strconv.FormatInt(contentRange.getLength(), 10))
|
||||||
w.Header().Set("Content-Range", contentRange.String())
|
w.Header().Set("Content-Range", contentRange.String())
|
||||||
w.WriteHeader(http.StatusPartialContent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func writePartSmallErrorResponse(w http.ResponseWriter, r *http.Request, err Par
|
|||||||
|
|
||||||
apiError := getAPIError(toAPIErrorCode(err))
|
apiError := getAPIError(toAPIErrorCode(err))
|
||||||
// Generate complete multipart error response.
|
// Generate complete multipart error response.
|
||||||
errorResponse := getAPIErrorResponse(apiError, r.URL.Path)
|
errorResponse := getAPIErrorResponse(apiError, r.URL.Path, w.Header().Get(responseRequestIDKey))
|
||||||
cmpErrResp := completeMultipartAPIError{err.PartSize, int64(5242880), err.PartNumber, err.PartETag, errorResponse}
|
cmpErrResp := completeMultipartAPIError{err.PartSize, int64(5242880), err.PartNumber, err.PartETag, errorResponse}
|
||||||
encodedErrorResponse := encodeResponse(cmpErrResp)
|
encodedErrorResponse := encodeResponse(cmpErrResp)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -577,7 +577,7 @@ func writeErrorResponse(w http.ResponseWriter, errorCode APIErrorCode, reqURL *u
|
|||||||
}
|
}
|
||||||
apiError := getAPIError(errorCode)
|
apiError := getAPIError(errorCode)
|
||||||
// Generate error response.
|
// Generate error response.
|
||||||
errorResponse := getAPIErrorResponse(apiError, reqURL.Path)
|
errorResponse := getAPIErrorResponse(apiError, reqURL.Path, w.Header().Get(responseRequestIDKey))
|
||||||
encodedErrorResponse := encodeResponse(errorResponse)
|
encodedErrorResponse := encodeResponse(errorResponse)
|
||||||
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeXML)
|
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeXML)
|
||||||
}
|
}
|
||||||
@@ -592,7 +592,7 @@ func writeErrorResponseHeadersOnly(w http.ResponseWriter, errorCode APIErrorCode
|
|||||||
func writeErrorResponseJSON(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) {
|
func writeErrorResponseJSON(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) {
|
||||||
apiError := getAPIError(errorCode)
|
apiError := getAPIError(errorCode)
|
||||||
// Generate error response.
|
// Generate error response.
|
||||||
errorResponse := getAPIErrorResponse(apiError, reqURL.Path)
|
errorResponse := getAPIErrorResponse(apiError, reqURL.Path, w.Header().Get(responseRequestIDKey))
|
||||||
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
||||||
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-10
@@ -20,7 +20,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/cmd/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// objectAPIHandler implements and provides http handlers for S3 API.
|
// objectAPIHandler implements and provides http handlers for S3 API.
|
||||||
@@ -31,14 +30,6 @@ type objectAPIHandlers struct {
|
|||||||
|
|
||||||
// registerAPIRouter - registers S3 compatible APIs.
|
// registerAPIRouter - registers S3 compatible APIs.
|
||||||
func registerAPIRouter(router *mux.Router) {
|
func registerAPIRouter(router *mux.Router) {
|
||||||
var err error
|
|
||||||
var cacheConfig = globalServerConfig.GetCacheConfig()
|
|
||||||
if len(cacheConfig.Drives) > 0 {
|
|
||||||
// initialize the new disk cache objects.
|
|
||||||
globalCacheObjectAPI, err = newServerCacheObjects(cacheConfig)
|
|
||||||
logger.FatalIf(err, "Unable to initialize disk caching")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize API.
|
// Initialize API.
|
||||||
api := objectAPIHandlers{
|
api := objectAPIHandlers{
|
||||||
ObjectAPI: newObjectLayerFn,
|
ObjectAPI: newObjectLayerFn,
|
||||||
@@ -71,6 +62,8 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
|
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(httpTraceAll(api.AbortMultipartUploadHandler)).Queries("uploadId", "{uploadId:.*}")
|
||||||
// GetObjectACL - this is a dummy call.
|
// GetObjectACL - this is a dummy call.
|
||||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectACLHandler)).Queries("acl", "")
|
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectACLHandler)).Queries("acl", "")
|
||||||
|
// SelectObjectContent
|
||||||
|
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.SelectObjectContentHandler)).Queries("select", "").Queries("select-type", "2")
|
||||||
// GetObject
|
// GetObject
|
||||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler))
|
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(httpTraceHdrs(api.GetObjectHandler))
|
||||||
// CopyObject
|
// CopyObject
|
||||||
@@ -108,7 +101,7 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
// HeadBucket
|
// HeadBucket
|
||||||
bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler))
|
bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler))
|
||||||
// PostPolicy
|
// PostPolicy
|
||||||
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceAll(api.PostPolicyBucketHandler))
|
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(httpTraceHdrs(api.PostPolicyBucketHandler))
|
||||||
// DeleteMultipleObjects
|
// DeleteMultipleObjects
|
||||||
bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "")
|
bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "")
|
||||||
// DeleteBucketPolicy
|
// DeleteBucketPolicy
|
||||||
|
|||||||
@@ -344,11 +344,14 @@ func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int6
|
|||||||
|
|
||||||
// Tests is requested authenticated function, tests replies for s3 errors.
|
// Tests is requested authenticated function, tests replies for s3 errors.
|
||||||
func TestIsReqAuthenticated(t *testing.T) {
|
func TestIsReqAuthenticated(t *testing.T) {
|
||||||
path, err := newTestConfig(globalMinioDefaultRegion)
|
objLayer, fsDir, err := prepareFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(fsDir)
|
||||||
|
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||||
t.Fatalf("unable initialize config file, %s", err)
|
t.Fatalf("unable initialize config file, %s", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(path)
|
|
||||||
|
|
||||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -384,11 +387,15 @@ func TestIsReqAuthenticated(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestCheckAdminRequestAuthType(t *testing.T) {
|
func TestCheckAdminRequestAuthType(t *testing.T) {
|
||||||
path, err := newTestConfig(globalMinioDefaultRegion)
|
objLayer, fsDir, err := prepareFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(fsDir)
|
||||||
|
|
||||||
|
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||||
t.Fatalf("unable initialize config file, %s", err)
|
t.Fatalf("unable initialize config file, %s", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(path)
|
|
||||||
|
|
||||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -138,12 +137,6 @@ func runPutObjectPartBenchmark(b *testing.B, obj ObjectLayer, partSize int) {
|
|||||||
|
|
||||||
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function.
|
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectPartBenchmark function.
|
||||||
func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Unable to initialize config. %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -151,18 +144,13 @@ func benchmarkPutObjectPart(b *testing.B, instanceType string, objSize int) {
|
|||||||
}
|
}
|
||||||
// cleaning up the backend by removing all the directories and files created on function return.
|
// cleaning up the backend by removing all the directories and files created on function return.
|
||||||
defer removeRoots(disks)
|
defer removeRoots(disks)
|
||||||
|
|
||||||
// uses *testing.B and the object Layer to run the benchmark.
|
// uses *testing.B and the object Layer to run the benchmark.
|
||||||
runPutObjectPartBenchmark(b, objLayer, objSize)
|
runPutObjectPartBenchmark(b, objLayer, objSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function.
|
// creates XL/FS backend setup, obtains the object layer and calls the runPutObjectBenchmark function.
|
||||||
func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Unable to initialize config. %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -170,18 +158,13 @@ func benchmarkPutObject(b *testing.B, instanceType string, objSize int) {
|
|||||||
}
|
}
|
||||||
// cleaning up the backend by removing all the directories and files created on function return.
|
// cleaning up the backend by removing all the directories and files created on function return.
|
||||||
defer removeRoots(disks)
|
defer removeRoots(disks)
|
||||||
|
|
||||||
// uses *testing.B and the object Layer to run the benchmark.
|
// uses *testing.B and the object Layer to run the benchmark.
|
||||||
runPutObjectBenchmark(b, objLayer, objSize)
|
runPutObjectBenchmark(b, objLayer, objSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for put object.
|
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for put object.
|
||||||
func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) {
|
func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Unable to initialize config. %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -189,6 +172,7 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
|
|||||||
}
|
}
|
||||||
// cleaning up the backend by removing all the directories and files created on function return.
|
// cleaning up the backend by removing all the directories and files created on function return.
|
||||||
defer removeRoots(disks)
|
defer removeRoots(disks)
|
||||||
|
|
||||||
// uses *testing.B and the object Layer to run the benchmark.
|
// uses *testing.B and the object Layer to run the benchmark.
|
||||||
runPutObjectBenchmarkParallel(b, objLayer, objSize)
|
runPutObjectBenchmarkParallel(b, objLayer, objSize)
|
||||||
}
|
}
|
||||||
@@ -196,16 +180,10 @@ func benchmarkPutObjectParallel(b *testing.B, instanceType string, objSize int)
|
|||||||
// Benchmark utility functions for ObjectLayer.GetObject().
|
// Benchmark utility functions for ObjectLayer.GetObject().
|
||||||
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
|
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
|
||||||
func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
func runGetObjectBenchmark(b *testing.B, obj ObjectLayer, objSize int) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Unable to initialize config. %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
// obtains random bucket name.
|
// obtains random bucket name.
|
||||||
bucket := getRandomBucketName()
|
bucket := getRandomBucketName()
|
||||||
// create bucket.
|
// create bucket.
|
||||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -269,12 +247,6 @@ func generateBytesData(size int) []byte {
|
|||||||
|
|
||||||
// creates XL/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function.
|
// creates XL/FS backend setup, obtains the object layer and calls the runGetObjectBenchmark function.
|
||||||
func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Unable to initialize config. %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -282,18 +254,13 @@ func benchmarkGetObject(b *testing.B, instanceType string, objSize int) {
|
|||||||
}
|
}
|
||||||
// cleaning up the backend by removing all the directories and files created.
|
// cleaning up the backend by removing all the directories and files created.
|
||||||
defer removeRoots(disks)
|
defer removeRoots(disks)
|
||||||
|
|
||||||
// uses *testing.B and the object Layer to run the benchmark.
|
// uses *testing.B and the object Layer to run the benchmark.
|
||||||
runGetObjectBenchmark(b, objLayer, objSize)
|
runGetObjectBenchmark(b, objLayer, objSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() .
|
// creates XL/FS backend setup, obtains the object layer and runs parallel benchmark for ObjectLayer.GetObject() .
|
||||||
func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) {
|
func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Unable to initialize config. %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
// create a temp XL/FS backend.
|
// create a temp XL/FS backend.
|
||||||
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
objLayer, disks, err := prepareBenchmarkBackend(instanceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -301,6 +268,7 @@ func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int)
|
|||||||
}
|
}
|
||||||
// cleaning up the backend by removing all the directories and files created.
|
// cleaning up the backend by removing all the directories and files created.
|
||||||
defer removeRoots(disks)
|
defer removeRoots(disks)
|
||||||
|
|
||||||
// uses *testing.B and the object Layer to run the benchmark.
|
// uses *testing.B and the object Layer to run the benchmark.
|
||||||
runGetObjectBenchmarkParallel(b, objLayer, objSize)
|
runGetObjectBenchmarkParallel(b, objLayer, objSize)
|
||||||
}
|
}
|
||||||
@@ -308,16 +276,10 @@ func benchmarkGetObjectParallel(b *testing.B, instanceType string, objSize int)
|
|||||||
// Parallel benchmark utility functions for ObjectLayer.PutObject().
|
// Parallel benchmark utility functions for ObjectLayer.PutObject().
|
||||||
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
|
// Creates Object layer setup ( MakeBucket ) and then runs the PutObject benchmark.
|
||||||
func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Unable to initialize config. %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
// obtains random bucket name.
|
// obtains random bucket name.
|
||||||
bucket := getRandomBucketName()
|
bucket := getRandomBucketName()
|
||||||
// create bucket.
|
// create bucket.
|
||||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -359,16 +321,10 @@ func runPutObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
|||||||
// Parallel benchmark utility functions for ObjectLayer.GetObject().
|
// Parallel benchmark utility functions for ObjectLayer.GetObject().
|
||||||
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
|
// Creates Object layer setup ( MakeBucket, PutObject) and then runs the benchmark.
|
||||||
func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
func runGetObjectBenchmarkParallel(b *testing.B, obj ObjectLayer, objSize int) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Unable to initialize config. %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
// obtains random bucket name.
|
// obtains random bucket name.
|
||||||
bucket := getRandomBucketName()
|
bucket := getRandomBucketName()
|
||||||
// create bucket.
|
// create bucket.
|
||||||
err = obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
err := obj.MakeBucketWithLocation(context.Background(), bucket, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"hash"
|
||||||
|
|
||||||
|
"github.com/minio/highwayhash"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/sha256-simd"
|
||||||
|
"golang.org/x/crypto/blake2b"
|
||||||
|
)
|
||||||
|
|
||||||
|
// magic HH-256 key as HH-256 hash of the first 100 decimals of π as utf-8 string with a zero key.
|
||||||
|
var magicHighwayHash256Key = []byte("\x4b\xe7\x34\xfa\x8e\x23\x8a\xcd\x26\x3e\x83\xe6\xbb\x96\x85\x52\x04\x0f\x93\x5d\xa3\x9f\x44\x14\x97\xe0\x9d\x13\x22\xde\x36\xa0")
|
||||||
|
|
||||||
|
// BitrotAlgorithm specifies a algorithm used for bitrot protection.
|
||||||
|
type BitrotAlgorithm uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SHA256 represents the SHA-256 hash function
|
||||||
|
SHA256 BitrotAlgorithm = 1 + iota
|
||||||
|
// HighwayHash256 represents the HighwayHash-256 hash function
|
||||||
|
HighwayHash256
|
||||||
|
// BLAKE2b512 represents the BLAKE2b-512 hash function
|
||||||
|
BLAKE2b512
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultBitrotAlgorithm is the default algorithm used for bitrot protection.
|
||||||
|
const (
|
||||||
|
DefaultBitrotAlgorithm = HighwayHash256
|
||||||
|
)
|
||||||
|
|
||||||
|
var bitrotAlgorithms = map[BitrotAlgorithm]string{
|
||||||
|
SHA256: "sha256",
|
||||||
|
BLAKE2b512: "blake2b",
|
||||||
|
HighwayHash256: "highwayhash256",
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new hash.Hash calculating the given bitrot algorithm.
|
||||||
|
func (a BitrotAlgorithm) New() hash.Hash {
|
||||||
|
switch a {
|
||||||
|
case SHA256:
|
||||||
|
return sha256.New()
|
||||||
|
case BLAKE2b512:
|
||||||
|
b2, _ := blake2b.New512(nil) // New512 never returns an error if the key is nil
|
||||||
|
return b2
|
||||||
|
case HighwayHash256:
|
||||||
|
hh, _ := highwayhash.New(magicHighwayHash256Key) // New will never return error since key is 256 bit
|
||||||
|
return hh
|
||||||
|
default:
|
||||||
|
logger.CriticalIf(context.Background(), errors.New("Unsupported bitrot algorithm"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available reports whether the given algorihm is available.
|
||||||
|
func (a BitrotAlgorithm) Available() bool {
|
||||||
|
_, ok := bitrotAlgorithms[a]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string identifier for a given bitrot algorithm.
|
||||||
|
// If the algorithm is not supported String panics.
|
||||||
|
func (a BitrotAlgorithm) String() string {
|
||||||
|
name, ok := bitrotAlgorithms[a]
|
||||||
|
if !ok {
|
||||||
|
logger.CriticalIf(context.Background(), errors.New("Unsupported bitrot algorithm"))
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitrotVerifier returns a new BitrotVerifier implementing the given algorithm.
|
||||||
|
func NewBitrotVerifier(algorithm BitrotAlgorithm, checksum []byte) *BitrotVerifier {
|
||||||
|
return &BitrotVerifier{algorithm, checksum}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitrotVerifier can be used to verify protected data.
|
||||||
|
type BitrotVerifier struct {
|
||||||
|
algorithm BitrotAlgorithm
|
||||||
|
sum []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitrotAlgorithmFromString returns a bitrot algorithm from the given string representation.
|
||||||
|
// It returns 0 if the string representation does not match any supported algorithm.
|
||||||
|
// The zero value of a bitrot algorithm is never supported.
|
||||||
|
func BitrotAlgorithmFromString(s string) (a BitrotAlgorithm) {
|
||||||
|
for alg, name := range bitrotAlgorithms {
|
||||||
|
if name == s {
|
||||||
|
return alg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// To read bit-rot verified data.
|
||||||
|
type bitrotReader struct {
|
||||||
|
disk StorageAPI
|
||||||
|
volume string
|
||||||
|
filePath string
|
||||||
|
verifier *BitrotVerifier // Holds the bit-rot info
|
||||||
|
endOffset int64 // Affects the length of data requested in disk.ReadFile depending on Read()'s offset
|
||||||
|
buf []byte // Holds bit-rot verified data
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBitrotReader returns bitrotReader.
|
||||||
|
// Note that the buffer is allocated later in Read(). This is because we will know the buffer length only
|
||||||
|
// during the bitrotReader.Read(). Depending on when parallelReader fails-over, the buffer length can be different.
|
||||||
|
func newBitrotReader(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm, endOffset int64, sum []byte) *bitrotReader {
|
||||||
|
return &bitrotReader{
|
||||||
|
disk: disk,
|
||||||
|
volume: volume,
|
||||||
|
filePath: filePath,
|
||||||
|
verifier: &BitrotVerifier{algo, sum},
|
||||||
|
endOffset: endOffset,
|
||||||
|
buf: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadChunk returns requested data.
|
||||||
|
func (b *bitrotReader) ReadChunk(offset int64, length int64) ([]byte, error) {
|
||||||
|
if b.buf == nil {
|
||||||
|
b.buf = make([]byte, b.endOffset-offset)
|
||||||
|
if _, err := b.disk.ReadFile(b.volume, b.filePath, offset, b.buf, b.verifier); err != nil {
|
||||||
|
logger.LogIf(context.Background(), err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if int64(len(b.buf)) < length {
|
||||||
|
logger.LogIf(context.Background(), errLessData)
|
||||||
|
return nil, errLessData
|
||||||
|
}
|
||||||
|
retBuf := b.buf[:length]
|
||||||
|
b.buf = b.buf[length:]
|
||||||
|
return retBuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// To calculate the bit-rot of the written data.
|
||||||
|
type bitrotWriter struct {
|
||||||
|
disk StorageAPI
|
||||||
|
volume string
|
||||||
|
filePath string
|
||||||
|
h hash.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBitrotWriter returns bitrotWriter.
|
||||||
|
func newBitrotWriter(disk StorageAPI, volume, filePath string, algo BitrotAlgorithm) *bitrotWriter {
|
||||||
|
return &bitrotWriter{
|
||||||
|
disk: disk,
|
||||||
|
volume: volume,
|
||||||
|
filePath: filePath,
|
||||||
|
h: algo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append appends the data and while calculating the hash.
|
||||||
|
func (b *bitrotWriter) Append(buf []byte) error {
|
||||||
|
n, err := b.h.Write(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n != len(buf) {
|
||||||
|
logger.LogIf(context.Background(), errUnexpected)
|
||||||
|
return errUnexpected
|
||||||
|
}
|
||||||
|
if err = b.disk.AppendFile(b.volume, b.filePath, buf); err != nil {
|
||||||
|
logger.LogIf(context.Background(), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum returns bit-rot sum.
|
||||||
|
func (b *bitrotWriter) Sum() []byte {
|
||||||
|
return b.h.Sum(nil)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBitrotReaderWriter(t *testing.T) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
volume := "testvol"
|
||||||
|
filePath := "testfile"
|
||||||
|
|
||||||
|
disk, err := newPosix(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
disk.MakeVol(volume)
|
||||||
|
|
||||||
|
writer := newBitrotWriter(disk, volume, filePath, HighwayHash256)
|
||||||
|
|
||||||
|
err = writer.Append([]byte("aaaaaaaaa"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = writer.Append([]byte("a"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = writer.Append([]byte("aaaaaaaaaa"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = writer.Append([]byte("aaaaa"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = writer.Append([]byte("aaaaaaaaaa"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := newBitrotReader(disk, volume, filePath, HighwayHash256, 35, writer.Sum())
|
||||||
|
|
||||||
|
if _, err = reader.ReadChunk(0, 35); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-3
@@ -56,11 +56,12 @@ func (bf *BoolFlag) UnmarshalJSON(data []byte) (err error) {
|
|||||||
|
|
||||||
// ParseBoolFlag - parses string into BoolFlag.
|
// ParseBoolFlag - parses string into BoolFlag.
|
||||||
func ParseBoolFlag(s string) (bf BoolFlag, err error) {
|
func ParseBoolFlag(s string) (bf BoolFlag, err error) {
|
||||||
if s == "on" {
|
switch s {
|
||||||
|
case "on":
|
||||||
bf = true
|
bf = true
|
||||||
} else if s == "off" {
|
case "off":
|
||||||
bf = false
|
bf = false
|
||||||
} else {
|
default:
|
||||||
err = fmt.Errorf("invalid value ‘%s’ for BoolFlag", s)
|
err = fmt.Errorf("invalid value ‘%s’ for BoolFlag", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIE
|
|||||||
// NOTE: It is recommended that this API to be used for application development.
|
// NOTE: It is recommended that this API to be used for application development.
|
||||||
// Minio continues to support ListObjectsV1 for supporting legacy tools.
|
// Minio continues to support ListObjectsV1 for supporting legacy tools.
|
||||||
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "ListObjectsV2")
|
ctx := newContext(r, w, "ListObjectsV2")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
@@ -80,17 +82,9 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// In ListObjectsV2 'continuation-token' is the marker.
|
|
||||||
marker := token
|
|
||||||
// Check if 'continuation-token' is empty.
|
|
||||||
if token == "" {
|
|
||||||
// Then we need to use 'start-after' as marker instead.
|
|
||||||
marker = startAfter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the query params before beginning to serve the request.
|
// Validate the query params before beginning to serve the request.
|
||||||
// fetch-owner is not validated since it is a boolean
|
// fetch-owner is not validated since it is a boolean
|
||||||
if s3Error := validateListObjectsArgs(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
if s3Error := validateListObjectsArgs(prefix, token, delimiter, maxKeys); s3Error != ErrNone {
|
||||||
writeErrorResponse(w, s3Error, r.URL)
|
writeErrorResponse(w, s3Error, r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -101,14 +95,14 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
// Inititate a list objects operation based on the input params.
|
// Inititate a list objects operation based on the input params.
|
||||||
// On success would return back ListObjectsInfo object to be
|
// On success would return back ListObjectsInfo object to be
|
||||||
// marshaled into S3 compatible XML header.
|
// marshaled into S3 compatible XML header.
|
||||||
listObjectsV2Info, err := listObjectsV2(ctx, bucket, prefix, marker, delimiter, maxKeys, fetchOwner, startAfter)
|
listObjectsV2Info, err := listObjectsV2(ctx, bucket, prefix, token, delimiter, maxKeys, fetchOwner, startAfter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsV2Info.Objects {
|
for i := range listObjectsV2Info.Objects {
|
||||||
if listObjectsV2Info.Objects[i].IsEncrypted() {
|
if crypto.IsEncrypted(listObjectsV2Info.Objects[i].UserDefined) {
|
||||||
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
listObjectsV2Info.Objects[i].Size, err = listObjectsV2Info.Objects[i].DecryptedSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
@@ -131,7 +125,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
// criteria to return a subset of the objects in a bucket.
|
// criteria to return a subset of the objects in a bucket.
|
||||||
//
|
//
|
||||||
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "ListObjectsV1")
|
ctx := newContext(r, w, "ListObjectsV1")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
@@ -174,7 +168,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range listObjectsInfo.Objects {
|
for i := range listObjectsInfo.Objects {
|
||||||
if listObjectsInfo.Objects[i].IsEncrypted() {
|
if crypto.IsEncrypted(listObjectsInfo.Objects[i].UserDefined) {
|
||||||
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
listObjectsInfo.Objects[i].Size, err = listObjectsInfo.Objects[i].DecryptedSize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
|
|||||||
+44
-62
@@ -28,20 +28,18 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
etcd "github.com/coreos/etcd/client"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/pkg/set"
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
|
"github.com/minio/minio/pkg/handlers"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
"github.com/minio/minio/pkg/sync/errgroup"
|
"github.com/minio/minio/pkg/sync/errgroup"
|
||||||
|
|
||||||
"github.com/minio/minio-go/pkg/set"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if there are buckets on server without corresponding entry in etcd backend and
|
// Check if there are buckets on server without corresponding entry in etcd backend and
|
||||||
@@ -64,7 +62,7 @@ func initFederatorBackend(objLayer ObjectLayer) {
|
|||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
r, gerr := globalDNSConfig.Get(b[index].Name)
|
r, gerr := globalDNSConfig.Get(b[index].Name)
|
||||||
if gerr != nil {
|
if gerr != nil {
|
||||||
if etcd.IsKeyNotFound(gerr) || gerr == dns.ErrNoEntriesFound {
|
if gerr == dns.ErrNoEntriesFound {
|
||||||
return globalDNSConfig.Put(b[index].Name)
|
return globalDNSConfig.Put(b[index].Name)
|
||||||
}
|
}
|
||||||
return gerr
|
return gerr
|
||||||
@@ -89,7 +87,7 @@ func initFederatorBackend(objLayer ObjectLayer) {
|
|||||||
// -------------------------
|
// -------------------------
|
||||||
// This operation returns bucket location.
|
// This operation returns bucket location.
|
||||||
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "GetBucketLocation")
|
ctx := newContext(r, w, "GetBucketLocation")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
@@ -105,12 +103,6 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
|
||||||
if err := bucketLock.GetRLock(globalObjectTimeout); err != nil {
|
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer bucketLock.RUnlock()
|
|
||||||
getBucketInfo := objectAPI.GetBucketInfo
|
getBucketInfo := objectAPI.GetBucketInfo
|
||||||
if api.CacheAPI() != nil {
|
if api.CacheAPI() != nil {
|
||||||
getBucketInfo = api.CacheAPI().GetBucketInfo
|
getBucketInfo = api.CacheAPI().GetBucketInfo
|
||||||
@@ -143,7 +135,7 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
|||||||
// uploads in the response.
|
// uploads in the response.
|
||||||
//
|
//
|
||||||
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "ListMultipartUploads")
|
ctx := newContext(r, w, "ListMultipartUploads")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
@@ -190,7 +182,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
|
|||||||
// This implementation of the GET operation returns a list of all buckets
|
// This implementation of the GET operation returns a list of all buckets
|
||||||
// owned by the authenticated sender of the request.
|
// owned by the authenticated sender of the request.
|
||||||
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "ListBuckets")
|
ctx := newContext(r, w, "ListBuckets")
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -211,7 +203,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
var bucketsInfo []BucketInfo
|
var bucketsInfo []BucketInfo
|
||||||
if globalDNSConfig != nil {
|
if globalDNSConfig != nil {
|
||||||
dnsBuckets, err := globalDNSConfig.List()
|
dnsBuckets, err := globalDNSConfig.List()
|
||||||
if err != nil && !etcd.IsKeyNotFound(err) && err != dns.ErrNoEntriesFound {
|
if err != nil && err != dns.ErrNoEntriesFound {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -246,7 +238,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
|
|
||||||
// DeleteMultipleObjectsHandler - deletes multiple objects.
|
// DeleteMultipleObjectsHandler - deletes multiple objects.
|
||||||
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "DeleteMultipleObjects")
|
ctx := newContext(r, w, "DeleteMultipleObjects")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
@@ -313,34 +305,24 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg = &sync.WaitGroup{} // Allocate a new wait group.
|
deleteObject := objectAPI.DeleteObject
|
||||||
var dErrs = make([]error, len(deleteObjects.Objects))
|
if api.CacheAPI() != nil {
|
||||||
|
deleteObject = api.CacheAPI().DeleteObject
|
||||||
// Delete all requested objects in parallel.
|
}
|
||||||
for index, object := range deleteObjects.Objects {
|
|
||||||
wg.Add(1)
|
var dErrs = make([]error, len(deleteObjects.Objects))
|
||||||
go func(i int, obj ObjectIdentifier) {
|
for index, object := range deleteObjects.Objects {
|
||||||
defer wg.Done()
|
// If the request is denied access, each item
|
||||||
// If the request is denied access, each item
|
// should be marked as 'AccessDenied'
|
||||||
// should be marked as 'AccessDenied'
|
if s3Error == ErrAccessDenied {
|
||||||
if s3Error == ErrAccessDenied {
|
dErrs[index] = PrefixAccessDenied{
|
||||||
dErrs[i] = PrefixAccessDenied{
|
Bucket: bucket,
|
||||||
Bucket: bucket,
|
Object: object.ObjectName,
|
||||||
Object: obj.ObjectName,
|
}
|
||||||
}
|
continue
|
||||||
return
|
}
|
||||||
}
|
dErrs[index] = deleteObject(ctx, bucket, object.ObjectName)
|
||||||
deleteObject := objectAPI.DeleteObject
|
|
||||||
if api.CacheAPI() != nil {
|
|
||||||
deleteObject = api.CacheAPI().DeleteObject
|
|
||||||
}
|
|
||||||
dErr := deleteObject(ctx, bucket, obj.ObjectName)
|
|
||||||
if dErr != nil {
|
|
||||||
dErrs[i] = dErr
|
|
||||||
}
|
|
||||||
}(index, object)
|
|
||||||
}
|
}
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Collect deleted objects and errors if any.
|
// Collect deleted objects and errors if any.
|
||||||
var deletedObjects []ObjectIdentifier
|
var deletedObjects []ObjectIdentifier
|
||||||
@@ -375,7 +357,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
// Get host and port from Request.RemoteAddr failing which
|
// Get host and port from Request.RemoteAddr failing which
|
||||||
// fill them with empty strings.
|
// fill them with empty strings.
|
||||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host, port = "", ""
|
host, port = "", ""
|
||||||
}
|
}
|
||||||
@@ -400,7 +382,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
// ----------
|
// ----------
|
||||||
// This implementation of the PUT operation creates a new bucket for authenticated request
|
// This implementation of the PUT operation creates a new bucket for authenticated request
|
||||||
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "PutBucket")
|
ctx := newContext(r, w, "PutBucket")
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -432,7 +414,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
if globalDNSConfig != nil {
|
if globalDNSConfig != nil {
|
||||||
if _, err := globalDNSConfig.Get(bucket); err != nil {
|
if _, err := globalDNSConfig.Get(bucket); err != nil {
|
||||||
if etcd.IsKeyNotFound(err) || err == dns.ErrNoEntriesFound {
|
if err == dns.ErrNoEntriesFound {
|
||||||
// Proceed to creating a bucket.
|
// Proceed to creating a bucket.
|
||||||
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location); err != nil {
|
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location); err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
@@ -476,7 +458,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
// This implementation of the POST operation handles object creation with a specified
|
// This implementation of the POST operation handles object creation with a specified
|
||||||
// signature policy in multipart/form-data
|
// signature policy in multipart/form-data
|
||||||
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "PostPolicyBucket")
|
ctx := newContext(r, w, "PostPolicyBucket")
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -601,7 +583,8 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract metadata to be saved from received Form.
|
// Extract metadata to be saved from received Form.
|
||||||
metadata, err := extractMetadataFromHeader(ctx, formValues)
|
metadata := make(map[string]string)
|
||||||
|
err = extractMetadataFromMap(ctx, formValues, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||||
return
|
return
|
||||||
@@ -615,15 +598,17 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
}
|
}
|
||||||
|
|
||||||
if objectAPI.IsEncryptionSupported() {
|
if objectAPI.IsEncryptionSupported() {
|
||||||
if hasSSECustomerHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C requests
|
if hasServerSideEncryptionHeader(formValues) && !hasSuffix(object, slashSeparator) { // handle SSE-C and SSE-S3 requests
|
||||||
var reader io.Reader
|
var reader io.Reader
|
||||||
var key []byte
|
var key []byte
|
||||||
key, err = ParseSSECustomerHeader(formValues)
|
if crypto.SSEC.IsRequested(formValues) {
|
||||||
if err != nil {
|
key, err = ParseSSECustomerHeader(formValues)
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
if err != nil {
|
||||||
return
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
reader, err = newEncryptReader(hashReader, key, metadata)
|
reader, err = newEncryptReader(hashReader, key, bucket, object, metadata, crypto.S3.IsRequested(formValues))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -648,7 +633,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
w.Header().Set("Location", location)
|
w.Header().Set("Location", location)
|
||||||
|
|
||||||
// Get host and port from Request.RemoteAddr.
|
// Get host and port from Request.RemoteAddr.
|
||||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host, port = "", ""
|
host, port = "", ""
|
||||||
}
|
}
|
||||||
@@ -695,7 +680,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
// have permission to access it. Otherwise, the operation might
|
// have permission to access it. Otherwise, the operation might
|
||||||
// return responses such as 404 Not Found and 403 Forbidden.
|
// return responses such as 404 Not Found and 403 Forbidden.
|
||||||
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "HeadBucket")
|
ctx := newContext(r, w, "HeadBucket")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
@@ -725,7 +710,7 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
// DeleteBucketHandler - Delete bucket
|
// DeleteBucketHandler - Delete bucket
|
||||||
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "DeleteBucket")
|
ctx := newContext(r, w, "DeleteBucket")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
@@ -753,10 +738,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
|
|
||||||
globalNotificationSys.RemoveNotification(bucket)
|
globalNotificationSys.RemoveNotification(bucket)
|
||||||
globalPolicySys.Remove(bucket)
|
globalPolicySys.Remove(bucket)
|
||||||
for nerr := range globalNotificationSys.DeleteBucket(bucket) {
|
globalNotificationSys.DeleteBucket(ctx, bucket)
|
||||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
|
||||||
logger.LogIf(ctx, nerr.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalDNSConfig != nil {
|
if globalDNSConfig != nil {
|
||||||
if err := globalDNSConfig.Delete(bucket); err != nil {
|
if err := globalDNSConfig.Delete(bucket); err != nil {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ var errNoSuchNotifications = errors.New("The specified bucket does not have buck
|
|||||||
// as per http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
|
// as per http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
|
||||||
// It returns empty configuration if its not set.
|
// It returns empty configuration if its not set.
|
||||||
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "GetBucketNotification")
|
ctx := newContext(r, w, "GetBucketNotification")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucketName := vars["bucket"]
|
bucketName := vars["bucket"]
|
||||||
@@ -94,7 +94,7 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
// PutBucketNotificationHandler - This HTTP handler stores given notification configuration as per
|
// PutBucketNotificationHandler - This HTTP handler stores given notification configuration as per
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
|
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
|
||||||
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "PutBucketNotification")
|
ctx := newContext(r, w, "PutBucketNotification")
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
objectAPI := api.ObjectAPI()
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
@@ -146,10 +146,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
rulesMap := config.ToRulesMap()
|
rulesMap := config.ToRulesMap()
|
||||||
globalNotificationSys.AddRulesMap(bucketName, rulesMap)
|
globalNotificationSys.AddRulesMap(bucketName, rulesMap)
|
||||||
for nerr := range globalNotificationSys.PutBucketNotification(bucketName, rulesMap) {
|
globalNotificationSys.PutBucketNotification(ctx, bucketName, rulesMap)
|
||||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
|
||||||
logger.LogIf(ctx, nerr.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
@@ -157,7 +154,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
// ListenBucketNotificationHandler - This HTTP handler sends events to the connected HTTP client.
|
// ListenBucketNotificationHandler - This HTTP handler sends events to the connected HTTP client.
|
||||||
// Client should send prefix/suffix object name to match and events to watch as query parameters.
|
// Client should send prefix/suffix object name to match and events to watch as query parameters.
|
||||||
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "ListenBucketNotification")
|
ctx := newContext(r, w, "ListenBucketNotification")
|
||||||
|
|
||||||
// Validate if bucket exists.
|
// Validate if bucket exists.
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
@@ -238,7 +235,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
|
|
||||||
rulesMap := event.NewRulesMap(eventNames, pattern, target.ID())
|
rulesMap := event.NewRulesMap(eventNames, pattern, target.ID())
|
||||||
|
|
||||||
if err := globalNotificationSys.AddRemoteTarget(bucketName, target, rulesMap); err != nil {
|
if err = globalNotificationSys.AddRemoteTarget(bucketName, target, rulesMap); err != nil {
|
||||||
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
@@ -260,11 +257,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
errCh := globalNotificationSys.ListenBucketNotification(bucketName, eventNames, pattern, target.ID(), *thisAddr)
|
globalNotificationSys.ListenBucketNotification(ctx, bucketName, eventNames, pattern, target.ID(), *thisAddr)
|
||||||
for nerr := range errCh {
|
|
||||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
|
||||||
logger.LogIf(ctx, nerr.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
<-target.DoneCh
|
<-target.DoneCh
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const (
|
|||||||
// PutBucketPolicyHandler - This HTTP handler stores given bucket policy configuration as per
|
// PutBucketPolicyHandler - This HTTP handler stores given bucket policy configuration as per
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
||||||
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "PutBucketPolicy")
|
ctx := newContext(r, w, "PutBucketPolicy")
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
@@ -91,10 +91,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalPolicySys.Set(bucket, *bucketPolicy)
|
globalPolicySys.Set(bucket, *bucketPolicy)
|
||||||
for nerr := range globalNotificationSys.SetBucketPolicy(bucket, bucketPolicy) {
|
globalNotificationSys.SetBucketPolicy(ctx, bucket, bucketPolicy)
|
||||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
|
||||||
logger.LogIf(ctx, nerr.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
@@ -102,7 +99,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
|
|
||||||
// DeleteBucketPolicyHandler - This HTTP handler removes bucket policy configuration.
|
// DeleteBucketPolicyHandler - This HTTP handler removes bucket policy configuration.
|
||||||
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "DeleteBucketPolicy")
|
ctx := newContext(r, w, "DeleteBucketPolicy")
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
@@ -130,10 +127,7 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalPolicySys.Remove(bucket)
|
globalPolicySys.Remove(bucket)
|
||||||
for nerr := range globalNotificationSys.RemoveBucketPolicy(bucket) {
|
globalNotificationSys.RemoveBucketPolicy(ctx, bucket)
|
||||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
|
||||||
logger.LogIf(ctx, nerr.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
@@ -141,7 +135,7 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
|||||||
|
|
||||||
// GetBucketPolicyHandler - This HTTP handler returns bucket policy configuration.
|
// GetBucketPolicyHandler - This HTTP handler returns bucket policy configuration.
|
||||||
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, "GetBucketPolicy")
|
ctx := newContext(r, w, "GetBucketPolicy")
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
objAPI := api.ObjectAPI()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
|
|||||||
@@ -437,11 +437,13 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
|
|
||||||
if recV4.Code != testCase.expectedRespStatus {
|
if recV4.Code != testCase.expectedRespStatus {
|
||||||
// Verify whether the bucket policy fetched is same as the one inserted.
|
// Verify whether the bucket policy fetched is same as the one inserted.
|
||||||
expectedPolicy, err := policy.ParseConfig(strings.NewReader(expectedBucketPolicyStr), testCase.bucketName)
|
var expectedPolicy *policy.Policy
|
||||||
|
expectedPolicy, err = policy.ParseConfig(strings.NewReader(expectedBucketPolicyStr), testCase.bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error. %v", err)
|
t.Fatalf("unexpected error. %v", err)
|
||||||
}
|
}
|
||||||
gotPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyReadBuf), testCase.bucketName)
|
var gotPolicy *policy.Policy
|
||||||
|
gotPolicy, err = policy.ParseConfig(bytes.NewReader(bucketPolicyReadBuf), testCase.bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error. %v", err)
|
t.Fatalf("unexpected error. %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-35
@@ -17,7 +17,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,9 +25,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
etcd "github.com/coreos/etcd/client"
|
etcd "github.com/coreos/etcd/clientv3"
|
||||||
|
|
||||||
"github.com/minio/cli"
|
"github.com/minio/cli"
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
@@ -48,34 +47,17 @@ func checkUpdate(mode string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize and load config from remote etcd or local config directory
|
// Load logger targets based on user's configuration
|
||||||
func initConfig() {
|
func loadLoggers() {
|
||||||
if globalEtcdClient != nil {
|
if globalServerConfig.Logger.Console.Enabled {
|
||||||
kapi := etcd.NewKeysAPI(globalEtcdClient)
|
// Enable console logging
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
logger.AddTarget(logger.NewConsole())
|
||||||
_, err := kapi.Get(ctx, getConfigFile(), nil)
|
|
||||||
cancel()
|
|
||||||
if err == nil {
|
|
||||||
logger.FatalIf(migrateConfig(), "Config migration failed.")
|
|
||||||
logger.FatalIf(loadConfig(), "Unable to load config version: '%s'.", serverConfigVersion)
|
|
||||||
} else {
|
|
||||||
if etcd.IsKeyNotFound(err) {
|
|
||||||
logger.FatalIf(newConfig(), "Unable to initialize minio config for the first time.")
|
|
||||||
logger.Info("Created minio configuration file successfully at %v", globalEtcdClient.Endpoints())
|
|
||||||
} else {
|
|
||||||
logger.FatalIf(err, "Unable to load config version: '%s'.", serverConfigVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
for _, l := range globalServerConfig.Logger.HTTP {
|
||||||
if isFile(getConfigFile()) {
|
if l.Enabled {
|
||||||
logger.FatalIf(migrateConfig(), "Config migration failed")
|
// Enable http logging
|
||||||
logger.FatalIf(loadConfig(), "Unable to load the configuration file")
|
logger.AddTarget(logger.NewHTTP(l.Endpoint, NewCustomHTTPTransport()))
|
||||||
} else {
|
}
|
||||||
// Config file does not exist, we create it fresh and return upon success.
|
|
||||||
logger.FatalIf(newConfig(), "Unable to initialize minio config for the first time")
|
|
||||||
logger.Info("Created minio configuration file successfully at " + getConfigDir())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +65,10 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
|||||||
|
|
||||||
var configDir string
|
var configDir string
|
||||||
|
|
||||||
if ctx.IsSet("config-dir") {
|
switch {
|
||||||
|
case ctx.IsSet("config-dir"):
|
||||||
configDir = ctx.String("config-dir")
|
configDir = ctx.String("config-dir")
|
||||||
} else if ctx.GlobalIsSet("config-dir") {
|
case ctx.GlobalIsSet("config-dir"):
|
||||||
configDir = ctx.GlobalString("config-dir")
|
configDir = ctx.GlobalString("config-dir")
|
||||||
// cli package does not expose parent's "config-dir" option. Below code is workaround.
|
// cli package does not expose parent's "config-dir" option. Below code is workaround.
|
||||||
if configDir == "" || configDir == getConfigDir() {
|
if configDir == "" || configDir == getConfigDir() {
|
||||||
@@ -93,7 +76,7 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
|||||||
configDir = ctx.Parent().GlobalString("config-dir")
|
configDir = ctx.Parent().GlobalString("config-dir")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
// Neither local nor global config-dir option is provided. In this case, try to use
|
// Neither local nor global config-dir option is provided. In this case, try to use
|
||||||
// default config directory.
|
// default config directory.
|
||||||
configDir = getConfigDir()
|
configDir = getConfigDir()
|
||||||
@@ -131,6 +114,11 @@ func handleCommonEnvVars() {
|
|||||||
globalActiveCred = cred
|
globalActiveCred = cred
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In distributed setup users need to set ENVs always.
|
||||||
|
if !globalIsEnvCreds && globalIsDistXL {
|
||||||
|
logger.Fatal(uiErrEnvCredentialsMissingServer(nil), "Unable to start distributed server mode")
|
||||||
|
}
|
||||||
|
|
||||||
if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
|
if browser := os.Getenv("MINIO_BROWSER"); browser != "" {
|
||||||
browserFlag, err := ParseBoolFlag(browser)
|
browserFlag, err := ParseBoolFlag(browser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -155,8 +143,9 @@ func handleCommonEnvVars() {
|
|||||||
etcdEndpoints := strings.Split(etcdEndpointsEnv, ",")
|
etcdEndpoints := strings.Split(etcdEndpointsEnv, ",")
|
||||||
var err error
|
var err error
|
||||||
globalEtcdClient, err = etcd.New(etcd.Config{
|
globalEtcdClient, err = etcd.New(etcd.Config{
|
||||||
Endpoints: etcdEndpoints,
|
Endpoints: etcdEndpoints,
|
||||||
Transport: NewCustomHTTPTransport(),
|
DialTimeout: defaultDialTimeout,
|
||||||
|
DialKeepAliveTime: defaultDialKeepAlive,
|
||||||
})
|
})
|
||||||
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
|
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
|
||||||
}
|
}
|
||||||
@@ -262,4 +251,18 @@ func handleCommonEnvVars() {
|
|||||||
globalIsEnvWORM = true
|
globalIsEnvWORM = true
|
||||||
globalWORMEnabled = bool(wormFlag)
|
globalWORMEnabled = bool(wormFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kmsConf, err := crypto.NewVaultConfig()
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Unable to initialize hashicorp vault")
|
||||||
|
}
|
||||||
|
if kmsConf.Vault.Endpoint != "" {
|
||||||
|
kms, err := crypto.NewVault(kmsConf)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Unable to initialize KMS")
|
||||||
|
}
|
||||||
|
globalKMS = kms
|
||||||
|
globalKMSKeyID = kmsConf.Vault.Key.Name
|
||||||
|
globalKMSConfig = kmsConf
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+235
-168
@@ -17,17 +17,19 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/event/target"
|
"github.com/minio/minio/pkg/event/target"
|
||||||
"github.com/minio/minio/pkg/quick"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Steps to move from version N to version N+1
|
// Steps to move from version N to version N+1
|
||||||
@@ -39,9 +41,9 @@ import (
|
|||||||
// 6. Make changes in config-current_test.go for any test change
|
// 6. Make changes in config-current_test.go for any test change
|
||||||
|
|
||||||
// Config version
|
// Config version
|
||||||
const serverConfigVersion = "26"
|
const serverConfigVersion = "28"
|
||||||
|
|
||||||
type serverConfig = serverConfigV26
|
type serverConfig = serverConfigV28
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// globalServerConfig server config.
|
// globalServerConfig server config.
|
||||||
@@ -67,6 +69,10 @@ func (s *serverConfig) GetRegion() string {
|
|||||||
|
|
||||||
// SetCredential sets new credential and returns the previous credential.
|
// SetCredential sets new credential and returns the previous credential.
|
||||||
func (s *serverConfig) SetCredential(creds auth.Credentials) (prevCred auth.Credentials) {
|
func (s *serverConfig) SetCredential(creds auth.Credentials) (prevCred auth.Credentials) {
|
||||||
|
if creds.IsValid() && globalActiveCred.IsValid() {
|
||||||
|
globalActiveCred = creds
|
||||||
|
}
|
||||||
|
|
||||||
// Save previous credential.
|
// Save previous credential.
|
||||||
prevCred = s.Credential
|
prevCred = s.Credential
|
||||||
|
|
||||||
@@ -79,6 +85,9 @@ func (s *serverConfig) SetCredential(creds auth.Credentials) (prevCred auth.Cred
|
|||||||
|
|
||||||
// GetCredentials get current credentials.
|
// GetCredentials get current credentials.
|
||||||
func (s *serverConfig) GetCredential() auth.Credentials {
|
func (s *serverConfig) GetCredential() auth.Credentials {
|
||||||
|
if globalActiveCred.IsValid() {
|
||||||
|
return globalActiveCred
|
||||||
|
}
|
||||||
return s.Credential
|
return s.Credential
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,19 +137,117 @@ func (s *serverConfig) GetCacheConfig() CacheConfig {
|
|||||||
return s.Cache
|
return s.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save config file to corresponding backend
|
func (s *serverConfig) Validate() error {
|
||||||
func Save(configFile string, data interface{}) error {
|
if s.Version != serverConfigVersion {
|
||||||
return quick.SaveConfig(data, configFile, globalEtcdClient)
|
return fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", serverConfigVersion, s.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate credential fields only when
|
||||||
|
// they are not set via the environment
|
||||||
|
// Error out if global is env credential is not set and config has invalid credential
|
||||||
|
if !globalIsEnvCreds && !s.Credential.IsValid() {
|
||||||
|
return errors.New("invalid credential in config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Region: nothing to validate
|
||||||
|
// Browser, Worm, Cache and StorageClass values are already validated during json unmarshal
|
||||||
|
|
||||||
|
if s.Domain != "" {
|
||||||
|
if _, ok := dns.IsDomainName(s.Domain); !ok {
|
||||||
|
return errors.New("invalid domain name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.AMQP {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("amqp: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.Elasticsearch {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("elasticsearch: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.Kafka {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("kafka: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.MQTT {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("mqtt: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.MySQL {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("mysql: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.NATS {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("nats: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.PostgreSQL {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("postgreSQL: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.Redis {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("redis: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range s.Notify.Webhook {
|
||||||
|
if err := v.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("webhook: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load config from backend
|
func (s *serverConfig) loadFromEnvs() {
|
||||||
func Load(configFile string, data interface{}) (quick.Config, error) {
|
// If env is set override the credentials from config file.
|
||||||
return quick.LoadConfig(configFile, globalEtcdClient, data)
|
if globalIsEnvCreds {
|
||||||
}
|
s.SetCredential(globalActiveCred)
|
||||||
|
}
|
||||||
|
|
||||||
// GetVersion gets config version from backend
|
if globalIsEnvBrowser {
|
||||||
func GetVersion(configFile string) (string, error) {
|
s.SetBrowser(globalIsBrowserEnabled)
|
||||||
return quick.GetVersion(configFile, globalEtcdClient)
|
}
|
||||||
|
|
||||||
|
if globalIsEnvWORM {
|
||||||
|
s.SetWorm(globalWORMEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIsEnvRegion {
|
||||||
|
s.SetRegion(globalServerRegion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIsEnvDomainName {
|
||||||
|
s.Domain = globalDomainName
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIsStorageClass {
|
||||||
|
s.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalIsDiskCacheEnabled {
|
||||||
|
s.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalKMS != nil {
|
||||||
|
s.KMS = globalKMSConfig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the string describing a difference with the given
|
// Returns the string describing a difference with the given
|
||||||
@@ -180,6 +287,10 @@ func (s *serverConfig) ConfigDiff(t *serverConfig) string {
|
|||||||
return "MySQL Notification configuration differs"
|
return "MySQL Notification configuration differs"
|
||||||
case !reflect.DeepEqual(s.Notify.MQTT, t.Notify.MQTT):
|
case !reflect.DeepEqual(s.Notify.MQTT, t.Notify.MQTT):
|
||||||
return "MQTT Notification configuration differs"
|
return "MQTT Notification configuration differs"
|
||||||
|
case !reflect.DeepEqual(s.Logger, t.Logger):
|
||||||
|
return "Logger configuration differs"
|
||||||
|
case !reflect.DeepEqual(s.KMS, t.KMS):
|
||||||
|
return "KMS configuration differs"
|
||||||
case reflect.DeepEqual(s, t):
|
case reflect.DeepEqual(s, t):
|
||||||
return ""
|
return ""
|
||||||
default:
|
default:
|
||||||
@@ -208,6 +319,7 @@ func newServerConfig() *serverConfig {
|
|||||||
Expiry: globalCacheExpiry,
|
Expiry: globalCacheExpiry,
|
||||||
MaxUse: globalCacheMaxUse,
|
MaxUse: globalCacheMaxUse,
|
||||||
},
|
},
|
||||||
|
KMS: crypto.KMSConfig{},
|
||||||
Notify: notifier{},
|
Notify: notifier{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,163 +347,99 @@ func newServerConfig() *serverConfig {
|
|||||||
srvCfg.Cache.Exclude = make([]string, 0)
|
srvCfg.Cache.Exclude = make([]string, 0)
|
||||||
srvCfg.Cache.Expiry = globalCacheExpiry
|
srvCfg.Cache.Expiry = globalCacheExpiry
|
||||||
srvCfg.Cache.MaxUse = globalCacheMaxUse
|
srvCfg.Cache.MaxUse = globalCacheMaxUse
|
||||||
|
|
||||||
|
// Console logging is on by default
|
||||||
|
srvCfg.Logger.Console.Enabled = true
|
||||||
|
// Create an example of HTTP logger
|
||||||
|
srvCfg.Logger.HTTP = make(map[string]loggerHTTP)
|
||||||
|
srvCfg.Logger.HTTP["target1"] = loggerHTTP{Endpoint: "https://username:password@example.com/api"}
|
||||||
|
|
||||||
return srvCfg
|
return srvCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
// newConfig - initialize a new server config, saves env parameters if
|
func (s *serverConfig) loadToCachedConfigs() {
|
||||||
// found, otherwise use default parameters
|
|
||||||
func newConfig() error {
|
|
||||||
// Initialize server config.
|
|
||||||
srvCfg, err := newQuickConfig(newServerConfig())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If env is set override the credentials from config file.
|
|
||||||
if globalIsEnvCreds {
|
|
||||||
srvCfg.SetCredential(globalActiveCred)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsEnvBrowser {
|
|
||||||
srvCfg.SetBrowser(globalIsBrowserEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsEnvWORM {
|
|
||||||
srvCfg.SetWorm(globalWORMEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsEnvRegion {
|
|
||||||
srvCfg.SetRegion(globalServerRegion)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsEnvDomainName {
|
|
||||||
srvCfg.Domain = globalDomainName
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsStorageClass {
|
|
||||||
srvCfg.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsDiskCacheEnabled {
|
|
||||||
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hold the mutex lock before a new config is assigned.
|
|
||||||
// Save the new config globally.
|
|
||||||
// unlock the mutex.
|
|
||||||
globalServerConfigMu.Lock()
|
|
||||||
globalServerConfig = srvCfg
|
|
||||||
globalServerConfigMu.Unlock()
|
|
||||||
|
|
||||||
// Save config into file.
|
|
||||||
return Save(getConfigFile(), globalServerConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newQuickConfig - initialize a new server config, with an allocated
|
|
||||||
// quick.Config interface.
|
|
||||||
func newQuickConfig(srvCfg *serverConfig) (*serverConfig, error) {
|
|
||||||
qcfg, err := quick.NewConfig(srvCfg, globalEtcdClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
srvCfg.Config = qcfg
|
|
||||||
return srvCfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getValidConfig - returns valid server configuration
|
|
||||||
func getValidConfig() (*serverConfig, error) {
|
|
||||||
srvCfg := &serverConfig{
|
|
||||||
Region: globalMinioDefaultRegion,
|
|
||||||
Browser: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
srvCfg, err = newQuickConfig(srvCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
configFile := getConfigFile()
|
|
||||||
if err = srvCfg.Load(configFile); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if srvCfg.Version != serverConfigVersion {
|
|
||||||
return nil, fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", serverConfigVersion, srvCfg.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate credential fields only when
|
|
||||||
// they are not set via the environment
|
|
||||||
// Error out if global is env credential is not set and config has invalid credential
|
|
||||||
if !globalIsEnvCreds && !srvCfg.Credential.IsValid() {
|
|
||||||
return nil, errors.New("invalid credential in config file " + getConfigFile())
|
|
||||||
}
|
|
||||||
|
|
||||||
return srvCfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadConfig - loads a new config from disk, overrides params from env
|
|
||||||
// if found and valid
|
|
||||||
func loadConfig() error {
|
|
||||||
srvCfg, err := getValidConfig()
|
|
||||||
if err != nil {
|
|
||||||
return uiErrInvalidConfig(nil).Msg(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// If env is set override the credentials from config file.
|
|
||||||
if globalIsEnvCreds {
|
|
||||||
srvCfg.SetCredential(globalActiveCred)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsEnvBrowser {
|
|
||||||
srvCfg.SetBrowser(globalIsBrowserEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsEnvRegion {
|
|
||||||
srvCfg.SetRegion(globalServerRegion)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsEnvDomainName {
|
|
||||||
srvCfg.Domain = globalDomainName
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsStorageClass {
|
|
||||||
srvCfg.SetStorageClass(globalStandardStorageClass, globalRRStorageClass)
|
|
||||||
}
|
|
||||||
|
|
||||||
if globalIsDiskCacheEnabled {
|
|
||||||
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hold the mutex lock before a new config is assigned.
|
|
||||||
globalServerConfigMu.Lock()
|
|
||||||
globalServerConfig = srvCfg
|
|
||||||
if !globalIsEnvCreds {
|
if !globalIsEnvCreds {
|
||||||
globalActiveCred = globalServerConfig.GetCredential()
|
globalActiveCred = s.GetCredential()
|
||||||
}
|
}
|
||||||
if !globalIsEnvBrowser {
|
if !globalIsEnvBrowser {
|
||||||
globalIsBrowserEnabled = globalServerConfig.GetBrowser()
|
globalIsBrowserEnabled = s.GetBrowser()
|
||||||
}
|
}
|
||||||
if !globalIsEnvWORM {
|
if !globalIsEnvWORM {
|
||||||
globalWORMEnabled = globalServerConfig.GetWorm()
|
globalWORMEnabled = s.GetWorm()
|
||||||
}
|
}
|
||||||
if !globalIsEnvRegion {
|
if !globalIsEnvRegion {
|
||||||
globalServerRegion = globalServerConfig.GetRegion()
|
globalServerRegion = s.GetRegion()
|
||||||
}
|
}
|
||||||
if !globalIsEnvDomainName {
|
if !globalIsEnvDomainName {
|
||||||
globalDomainName = globalServerConfig.Domain
|
globalDomainName = s.Domain
|
||||||
}
|
}
|
||||||
if !globalIsStorageClass {
|
if !globalIsStorageClass {
|
||||||
globalStandardStorageClass, globalRRStorageClass = globalServerConfig.GetStorageClass()
|
globalStandardStorageClass, globalRRStorageClass = s.GetStorageClass()
|
||||||
}
|
}
|
||||||
if !globalIsDiskCacheEnabled {
|
if !globalIsDiskCacheEnabled {
|
||||||
cacheConf := globalServerConfig.GetCacheConfig()
|
cacheConf := s.GetCacheConfig()
|
||||||
globalCacheDrives = cacheConf.Drives
|
globalCacheDrives = cacheConf.Drives
|
||||||
globalCacheExcludes = cacheConf.Exclude
|
globalCacheExcludes = cacheConf.Exclude
|
||||||
globalCacheExpiry = cacheConf.Expiry
|
globalCacheExpiry = cacheConf.Expiry
|
||||||
globalCacheMaxUse = cacheConf.MaxUse
|
globalCacheMaxUse = cacheConf.MaxUse
|
||||||
}
|
}
|
||||||
|
if globalKMS == nil {
|
||||||
|
globalKMSConfig = s.KMS
|
||||||
|
if kms, err := crypto.NewVault(globalKMSConfig); err == nil {
|
||||||
|
globalKMS = kms
|
||||||
|
globalKMSKeyID = globalKMSConfig.Vault.Key.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConfig - initialize a new server config, saves env parameters if
|
||||||
|
// found, otherwise use default parameters
|
||||||
|
func newConfig(objAPI ObjectLayer) error {
|
||||||
|
// Initialize server config.
|
||||||
|
srvCfg := newServerConfig()
|
||||||
|
|
||||||
|
// Override any values from ENVs.
|
||||||
|
srvCfg.loadFromEnvs()
|
||||||
|
|
||||||
|
// Load values to cached global values.
|
||||||
|
srvCfg.loadToCachedConfigs()
|
||||||
|
|
||||||
|
// hold the mutex lock before a new config is assigned.
|
||||||
|
globalServerConfigMu.Lock()
|
||||||
|
globalServerConfig = srvCfg
|
||||||
|
globalServerConfigMu.Unlock()
|
||||||
|
|
||||||
|
// Save config into file.
|
||||||
|
return saveServerConfig(objAPI, globalServerConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getValidConfig - returns valid server configuration
|
||||||
|
func getValidConfig(objAPI ObjectLayer) (*serverConfig, error) {
|
||||||
|
srvCfg, err := readServerConfig(context.Background(), objAPI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return srvCfg, srvCfg.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig - loads a new config from disk, overrides params from env
|
||||||
|
// if found and valid
|
||||||
|
func loadConfig(objAPI ObjectLayer) error {
|
||||||
|
srvCfg, err := getValidConfig(objAPI)
|
||||||
|
if err != nil {
|
||||||
|
return uiErrInvalidConfig(nil).Msg(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override any values from ENVs.
|
||||||
|
srvCfg.loadFromEnvs()
|
||||||
|
|
||||||
|
// Load values to cached global values.
|
||||||
|
srvCfg.loadToCachedConfigs()
|
||||||
|
|
||||||
|
// hold the mutex lock before a new config is assigned.
|
||||||
|
globalServerConfigMu.Lock()
|
||||||
|
globalServerConfig = srvCfg
|
||||||
globalServerConfigMu.Unlock()
|
globalServerConfigMu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -402,17 +450,19 @@ func loadConfig() error {
|
|||||||
// * Add a new target in pkg/event/target package.
|
// * Add a new target in pkg/event/target package.
|
||||||
// * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>.
|
// * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>.
|
||||||
// * Handle the configuration in this function to create/add into TargetList.
|
// * Handle the configuration in this function to create/add into TargetList.
|
||||||
func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
func getNotificationTargets(config *serverConfig) *event.TargetList {
|
||||||
targetList := event.NewTargetList()
|
targetList := event.NewTargetList()
|
||||||
|
|
||||||
for id, args := range config.Notify.AMQP {
|
for id, args := range config.Notify.AMQP {
|
||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewAMQPTarget(id, args)
|
newTarget, err := target.NewAMQPTarget(id, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,10 +471,14 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
|||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewElasticsearchTarget(id, args)
|
newTarget, err := target.NewElasticsearchTarget(id, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
|
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,10 +487,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
|||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewKafkaTarget(id, args)
|
newTarget, err := target.NewKafkaTarget(id, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,10 +501,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
|||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewMQTTTarget(id, args)
|
newTarget, err := target.NewMQTTTarget(id, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,10 +515,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
|||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewMySQLTarget(id, args)
|
newTarget, err := target.NewMySQLTarget(id, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,10 +529,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
|||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewNATSTarget(id, args)
|
newTarget, err := target.NewNATSTarget(id, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,10 +543,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
|||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewPostgreSQLTarget(id, args)
|
newTarget, err := target.NewPostgreSQLTarget(id, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,10 +557,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
|||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget, err := target.NewRedisTarget(id, args)
|
newTarget, err := target.NewRedisTarget(id, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if err = targetList.Add(newTarget); err != nil {
|
if err = targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,10 +571,11 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
|||||||
if args.Enable {
|
if args.Enable {
|
||||||
newTarget := target.NewWebhookTarget(id, args)
|
newTarget := target.NewWebhookTarget(id, args)
|
||||||
if err := targetList.Add(newTarget); err != nil {
|
if err := targetList.Add(newTarget); err != nil {
|
||||||
return nil, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetList, nil
|
return targetList
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-45
@@ -17,9 +17,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
@@ -27,12 +26,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestServerConfig(t *testing.T) {
|
func TestServerConfig(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
objLayer, fsDir, err := prepareFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(fsDir)
|
||||||
|
|
||||||
|
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||||
t.Fatalf("Init Test config failed")
|
t.Fatalf("Init Test config failed")
|
||||||
}
|
}
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
if globalServerConfig.GetRegion() != globalMinioDefaultRegion {
|
if globalServerConfig.GetRegion() != globalMinioDefaultRegion {
|
||||||
t.Errorf("Expecting region `us-east-1` found %s", globalServerConfig.GetRegion())
|
t.Errorf("Expecting region `us-east-1` found %s", globalServerConfig.GetRegion())
|
||||||
@@ -49,16 +51,12 @@ func TestServerConfig(t *testing.T) {
|
|||||||
t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion)
|
t.Errorf("Expecting version %s found %s", globalServerConfig.GetVersion(), serverConfigVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to save.
|
if err := saveServerConfig(objLayer, globalServerConfig); err != nil {
|
||||||
if err := globalServerConfig.Save(getConfigFile()); err != nil {
|
|
||||||
t.Fatalf("Unable to save updated config file %s", err)
|
t.Fatalf("Unable to save updated config file %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do this only once here.
|
|
||||||
setConfigDir(rootPath)
|
|
||||||
|
|
||||||
// Initialize server config.
|
// Initialize server config.
|
||||||
if err := loadConfig(); err != nil {
|
if err := loadConfig(objLayer); err != nil {
|
||||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,23 +80,25 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
|||||||
|
|
||||||
defer resetGlobalIsEnvs()
|
defer resetGlobalIsEnvs()
|
||||||
|
|
||||||
// Get test root.
|
objLayer, fsDir, err := prepareFS()
|
||||||
rootPath, err := getTestRoot()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer os.RemoveAll(fsDir)
|
||||||
|
|
||||||
|
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||||
|
t.Fatalf("Init Test config failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
globalObjLayerMutex.Lock()
|
||||||
|
globalObjectAPI = objLayer
|
||||||
|
globalObjLayerMutex.Unlock()
|
||||||
|
|
||||||
serverHandleEnvVars()
|
serverHandleEnvVars()
|
||||||
|
|
||||||
// Do this only once here.
|
|
||||||
setConfigDir(rootPath)
|
|
||||||
|
|
||||||
// Init config
|
// Init config
|
||||||
initConfig()
|
initConfig()
|
||||||
|
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
// Check if serverConfig has
|
// Check if serverConfig has
|
||||||
if globalServerConfig.GetBrowser() {
|
if globalServerConfig.GetBrowser() {
|
||||||
t.Errorf("Expecting browser is set to false found %v", globalServerConfig.GetBrowser())
|
t.Errorf("Expecting browser is set to false found %v", globalServerConfig.GetBrowser())
|
||||||
@@ -127,16 +127,17 @@ func TestServerConfigWithEnvs(t *testing.T) {
|
|||||||
|
|
||||||
// Tests config validator..
|
// Tests config validator..
|
||||||
func TestValidateConfig(t *testing.T) {
|
func TestValidateConfig(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
objLayer, fsDir, err := prepareFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(fsDir)
|
||||||
|
|
||||||
|
if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||||
t.Fatalf("Init Test config failed")
|
t.Fatalf("Init Test config failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the root directory after the test ends.
|
configPath := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
configPath := filepath.Join(rootPath, minioConfigFile)
|
|
||||||
|
|
||||||
v := serverConfigVersion
|
v := serverConfigVersion
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -174,66 +175,66 @@ func TestValidateConfig(t *testing.T) {
|
|||||||
{`{"version": "` + v + `", "browser": "on", "browser": "on", "region":"us-east-1", "credential" : {"accessKey":"minio", "secretKey":"minio123"}}`, false},
|
{`{"version": "` + v + `", "browser": "on", "browser": "on", "region":"us-east-1", "credential" : {"accessKey":"minio", "secretKey":"minio123"}}`, false},
|
||||||
|
|
||||||
// Test 11 - Test AMQP
|
// Test 11 - Test AMQP
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false }}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "amqp": { "1": { "enable": true, "url": "", "exchange": "", "routingKey": "", "exchangeType": "", "mandatory": false, "immediate": false, "durable": false, "internal": false, "noWait": false, "autoDeleted": false }}}}`, false},
|
||||||
|
|
||||||
// Test 12 - Test NATS
|
// Test 12 - Test NATS
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "streaming": { "enable": false, "clusterID": "", "clientID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "nats": { "1": { "enable": true, "address": "", "subject": "", "username": "", "password": "", "token": "", "secure": false, "pingInterval": 0, "streaming": { "enable": false, "clusterID": "", "clientID": "", "async": false, "maxPubAcksInflight": 0 } } }}}`, false},
|
||||||
|
|
||||||
// Test 13 - Test ElasticSearch
|
// Test 13 - Test ElasticSearch
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "url": "", "index": "" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "url": "", "index": "" } }}}`, false},
|
||||||
|
|
||||||
// Test 14 - Test Redis
|
// Test 14 - Test Redis
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "address": "", "password": "", "key": "" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "address": "", "password": "", "key": "" } }}}`, false},
|
||||||
|
|
||||||
// Test 15 - Test PostgreSQL
|
// Test 15 - Test PostgreSQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, false},
|
||||||
|
|
||||||
// Test 16 - Test Kafka
|
// Test 16 - Test Kafka
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "kafka": { "1": { "enable": true, "brokers": null, "topic": "" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "kafka": { "1": { "enable": true, "brokers": null, "topic": "" } }}}`, false},
|
||||||
|
|
||||||
// Test 17 - Test Webhook
|
// Test 17 - Test Webhook
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "webhook": { "1": { "enable": true, "endpoint": "" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "webhook": { "1": { "enable": true, "endpoint": "" } }}}`, false},
|
||||||
|
|
||||||
// Test 18 - Test MySQL
|
// Test 18 - Test MySQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "table": "", "host": "", "port": "", "user": "", "password": "", "database": "" }}}}`, false},
|
||||||
|
|
||||||
// Test 19 - Test Format for MySQL
|
// Test 19 - Test Format for MySQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "invalid", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "invalid", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, false},
|
||||||
|
|
||||||
// Test 20 - Test valid Format for MySQL
|
// Test 20 - Test valid Format for MySQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "namespace", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mysql": { "1": { "enable": true, "dsnString": "", "format": "namespace", "table": "xxx", "host": "10.0.0.1", "port": "3306", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
||||||
|
|
||||||
// Test 21 - Test Format for PostgreSQL
|
// Test 21 - Test Format for PostgreSQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "invalid", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "invalid", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, false},
|
||||||
|
|
||||||
// Test 22 - Test valid Format for PostgreSQL
|
// Test 22 - Test valid Format for PostgreSQL
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "namespace", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "postgresql": { "1": { "enable": true, "connectionString": "", "format": "namespace", "table": "xxx", "host": "myhost", "port": "5432", "user": "abc", "password": "pqr", "database": "test1" }}}}`, true},
|
||||||
|
|
||||||
// Test 23 - Test Format for ElasticSearch
|
// Test 23 - Test Format for ElasticSearch
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "invalid", "url": "example.com", "index": "myindex" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "invalid", "url": "example.com", "index": "myindex" } }}}`, false},
|
||||||
|
|
||||||
// Test 24 - Test valid Format for ElasticSearch
|
// Test 24 - Test valid Format for ElasticSearch
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex" } }}}`, true},
|
||||||
|
|
||||||
// Test 25 - Test Format for Redis
|
// Test 25 - Test Format for Redis
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, false},
|
||||||
|
|
||||||
// Test 26 - Test valid Format for Redis
|
// Test 26 - Test valid Format for Redis
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},
|
||||||
|
|
||||||
// Test 27 - Test MQTT
|
// Test 27 - Test MQTT
|
||||||
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, true},
|
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "mqtt": { "1": { "enable": true, "broker": "", "topic": "", "qos": 0, "clientId": "", "username": "", "password": ""}}}}`, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
if werr := ioutil.WriteFile(configPath, []byte(testCase.configData), 0700); werr != nil {
|
if err = saveConfig(objLayer, configPath, []byte(testCase.configData)); err != nil {
|
||||||
t.Fatal(werr)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, verr := getValidConfig()
|
_, err = getValidConfig(objLayer)
|
||||||
if testCase.shouldPass && verr != nil {
|
if testCase.shouldPass && err != nil {
|
||||||
t.Errorf("Test %d, should pass but it failed with err = %v", i+1, verr)
|
t.Errorf("Test %d, should pass but it failed with err = %v", i+1, err)
|
||||||
}
|
}
|
||||||
if !testCase.shouldPass && verr == nil {
|
if !testCase.shouldPass && err == nil {
|
||||||
t.Errorf("Test %d, should fail but it succeeded.", i+1)
|
t.Errorf("Test %d, should fail but it succeeded.", i+1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,6 +320,18 @@ func TestConfigDiff(t *testing.T) {
|
|||||||
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: false}}}},
|
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: false}}}},
|
||||||
"MQTT Notification configuration differs",
|
"MQTT Notification configuration differs",
|
||||||
},
|
},
|
||||||
|
// 16
|
||||||
|
{
|
||||||
|
&serverConfig{Logger: loggerConfig{
|
||||||
|
Console: loggerConsole{Enabled: true},
|
||||||
|
HTTP: map[string]loggerHTTP{"1": {Endpoint: "http://address1"}},
|
||||||
|
}},
|
||||||
|
&serverConfig{Logger: loggerConfig{
|
||||||
|
Console: loggerConsole{Enabled: true},
|
||||||
|
HTTP: map[string]loggerHTTP{"1": {Endpoint: "http://address2"}},
|
||||||
|
}},
|
||||||
|
"Logger configuration differs",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ const (
|
|||||||
// Default minio configuration directory where below configuration files/directories are stored.
|
// Default minio configuration directory where below configuration files/directories are stored.
|
||||||
defaultMinioConfigDir = ".minio"
|
defaultMinioConfigDir = ".minio"
|
||||||
|
|
||||||
// Minio configuration file.
|
|
||||||
minioConfigFile = "config.json"
|
|
||||||
|
|
||||||
// Directory contains below files/directories for HTTPS configuration.
|
// Directory contains below files/directories for HTTPS configuration.
|
||||||
certsDir = "certs"
|
certsDir = "certs"
|
||||||
|
|
||||||
|
|||||||
+133
-3
@@ -17,13 +17,16 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
etcd "github.com/coreos/etcd/client"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
|
"github.com/minio/minio/pkg/dns"
|
||||||
"github.com/minio/minio/pkg/event"
|
"github.com/minio/minio/pkg/event"
|
||||||
"github.com/minio/minio/pkg/event/target"
|
"github.com/minio/minio/pkg/event/target"
|
||||||
xnet "github.com/minio/minio/pkg/net"
|
xnet "github.com/minio/minio/pkg/net"
|
||||||
@@ -33,7 +36,22 @@ import (
|
|||||||
// DO NOT EDIT following message template, please open a github issue to discuss instead.
|
// DO NOT EDIT following message template, please open a github issue to discuss instead.
|
||||||
var configMigrateMSGTemplate = "Configuration file %s migrated from version '%s' to '%s' successfully."
|
var configMigrateMSGTemplate = "Configuration file %s migrated from version '%s' to '%s' successfully."
|
||||||
|
|
||||||
// Migrates all config versions from "1" to "18".
|
// Save config file to corresponding backend
|
||||||
|
func Save(configFile string, data interface{}) error {
|
||||||
|
return quick.SaveConfig(data, configFile, globalEtcdClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config from backend
|
||||||
|
func Load(configFile string, data interface{}) (quick.Config, error) {
|
||||||
|
return quick.LoadConfig(configFile, globalEtcdClient, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVersion gets config version from backend
|
||||||
|
func GetVersion(configFile string) (string, error) {
|
||||||
|
return quick.GetVersion(configFile, globalEtcdClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrates all config versions from "1" to "28".
|
||||||
func migrateConfig() error {
|
func migrateConfig() error {
|
||||||
// Purge all configs with version '1',
|
// Purge all configs with version '1',
|
||||||
// this is a special case since version '1' used
|
// this is a special case since version '1' used
|
||||||
@@ -45,6 +63,9 @@ func migrateConfig() error {
|
|||||||
// Load only config version information.
|
// Load only config version information.
|
||||||
version, err := GetVersion(getConfigFile())
|
version, err := GetVersion(getConfigFile())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +209,16 @@ func migrateConfig() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
|
case "26":
|
||||||
|
if err = migrateV26ToV27(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case "27":
|
||||||
|
if err = migrateV27ToV28(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
case serverConfigVersion:
|
case serverConfigVersion:
|
||||||
// No migration needed. this always points to current version.
|
// No migration needed. this always points to current version.
|
||||||
err = nil
|
err = nil
|
||||||
@@ -201,7 +232,7 @@ func purgeV1() error {
|
|||||||
|
|
||||||
cv1 := &configV1{}
|
cv1 := &configV1{}
|
||||||
_, err := Load(configFile, cv1)
|
_, err := Load(configFile, cv1)
|
||||||
if os.IsNotExist(err) || etcd.IsKeyNotFound(err) {
|
if os.IsNotExist(err) || err == dns.ErrNoEntriesFound {
|
||||||
return nil
|
return nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return fmt.Errorf("Unable to load config version ‘1’. %v", err)
|
return fmt.Errorf("Unable to load config version ‘1’. %v", err)
|
||||||
@@ -2317,3 +2348,102 @@ func migrateV25ToV26() error {
|
|||||||
logger.Info(configMigrateMSGTemplate, configFile, cv25.Version, srvConfig.Version)
|
logger.Info(configMigrateMSGTemplate, configFile, cv25.Version, srvConfig.Version)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateV26ToV27() error {
|
||||||
|
configFile := getConfigFile()
|
||||||
|
|
||||||
|
// config V27 is backward compatible with V26, load the old
|
||||||
|
// config file in serverConfigV27 struct and put some examples
|
||||||
|
// in the new `logger` field
|
||||||
|
srvConfig := &serverConfigV27{}
|
||||||
|
_, err := quick.LoadConfig(configFile, globalEtcdClient, srvConfig)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if srvConfig.Version != "26" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
srvConfig.Version = "27"
|
||||||
|
// Enable console logging by default to avoid breaking users
|
||||||
|
// current deployments
|
||||||
|
srvConfig.Logger.Console.Enabled = true
|
||||||
|
srvConfig.Logger.HTTP = make(map[string]loggerHTTP)
|
||||||
|
srvConfig.Logger.HTTP["1"] = loggerHTTP{}
|
||||||
|
|
||||||
|
if err = quick.SaveConfig(srvConfig, configFile, globalEtcdClient); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘26’ to ‘27’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "26", "27")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateV27ToV28() error {
|
||||||
|
configFile := getConfigFile()
|
||||||
|
|
||||||
|
// config V28 is backward compatible with V27, load the old
|
||||||
|
// config file in serverConfigV28 struct and initialize KMSConfig
|
||||||
|
srvConfig := &serverConfigV28{}
|
||||||
|
_, err := quick.LoadConfig(configFile, globalEtcdClient, srvConfig)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if srvConfig.Version != "27" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
srvConfig.Version = "28"
|
||||||
|
srvConfig.KMS = crypto.KMSConfig{}
|
||||||
|
if err = quick.SaveConfig(srvConfig, configFile, globalEtcdClient); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrates '.minio.sys/config.json' v27 to v28.
|
||||||
|
func migrateMinioSysConfig(objAPI ObjectLayer) error {
|
||||||
|
// Construct path to config.json for the given bucket.
|
||||||
|
configFile := path.Join(bucketConfigPrefix, minioConfigFile)
|
||||||
|
transactionConfigFile := configFile + ".transaction"
|
||||||
|
|
||||||
|
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
||||||
|
// and configFile, take a transaction lock to avoid race.
|
||||||
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
||||||
|
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer objLock.Unlock()
|
||||||
|
|
||||||
|
return migrateV27ToV28MinioSys(objAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateV27ToV28MinioSys(objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
srvConfig, err := readServerConfig(context.Background(), objAPI)
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("Unable to load config file. %v", err)
|
||||||
|
}
|
||||||
|
if srvConfig.Version != "27" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
srvConfig.Version = "28"
|
||||||
|
srvConfig.KMS = crypto.KMSConfig{}
|
||||||
|
if err = saveServerConfig(objAPI, srvConfig); err != nil {
|
||||||
|
return fmt.Errorf("Failed to migrate config from ‘27’ to ‘28’. %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(configMigrateMSGTemplate, configFile, "27", "28")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+76
-28
@@ -25,15 +25,26 @@ import (
|
|||||||
|
|
||||||
// Test if config v1 is purged
|
// Test if config v1 is purged
|
||||||
func TestServerConfigMigrateV1(t *testing.T) {
|
func TestServerConfigMigrateV1(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
objLayer, fsDir, err := prepareFS()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(fsDir)
|
||||||
|
err = newTestConfig(globalMinioDefaultRegion, objLayer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Init Test config failed")
|
t.Fatalf("Init Test config failed")
|
||||||
}
|
}
|
||||||
// remove the root directory after the test ends.
|
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
defer os.RemoveAll(rootPath)
|
defer os.RemoveAll(rootPath)
|
||||||
|
|
||||||
setConfigDir(rootPath)
|
setConfigDir(rootPath)
|
||||||
|
|
||||||
|
globalObjLayerMutex.Lock()
|
||||||
|
globalObjectAPI = objLayer
|
||||||
|
globalObjLayerMutex.Unlock()
|
||||||
|
|
||||||
// Create a V1 config json file and store it
|
// Create a V1 config json file and store it
|
||||||
configJSON := "{ \"version\":\"1\", \"accessKeyId\":\"abcde\", \"secretAccessKey\":\"abcdefgh\"}"
|
configJSON := "{ \"version\":\"1\", \"accessKeyId\":\"abcde\", \"secretAccessKey\":\"abcdefgh\"}"
|
||||||
configPath := rootPath + "/fsUsers.json"
|
configPath := rootPath + "/fsUsers.json"
|
||||||
@@ -45,13 +56,14 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
|||||||
if err := migrateConfig(); err != nil {
|
if err := migrateConfig(); err != nil {
|
||||||
t.Fatal("Unexpected error: ", err)
|
t.Fatal("Unexpected error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if config v1 is removed from filesystem
|
// Check if config v1 is removed from filesystem
|
||||||
if _, err := os.Stat(configPath); err == nil || !os.IsNotExist(err) {
|
if _, err := os.Stat(configPath); err == nil || !os.IsNotExist(err) {
|
||||||
t.Fatal("Config V1 file is not purged")
|
t.Fatal("Config V1 file is not purged")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize server config and check again if everything is fine
|
// Initialize server config and check again if everything is fine
|
||||||
if err := loadConfig(); err != nil {
|
if err := loadConfig(objLayer); err != nil {
|
||||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,20 +71,13 @@ func TestServerConfigMigrateV1(t *testing.T) {
|
|||||||
// Test if all migrate code returns nil when config file does not
|
// Test if all migrate code returns nil when config file does not
|
||||||
// exist
|
// exist
|
||||||
func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Init Test config failed")
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
defer os.RemoveAll(rootPath)
|
||||||
|
|
||||||
setConfigDir(rootPath)
|
setConfigDir(rootPath)
|
||||||
configPath := rootPath + "/" + minioConfigFile
|
|
||||||
|
|
||||||
// Remove config file
|
|
||||||
if err := os.Remove(configPath); err != nil {
|
|
||||||
t.Fatal("Unexpected error: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := migrateV2ToV3(); err != nil {
|
if err := migrateV2ToV3(); err != nil {
|
||||||
t.Fatal("migrate v2 to v3 should succeed when no config file is found")
|
t.Fatal("migrate v2 to v3 should succeed when no config file is found")
|
||||||
@@ -134,18 +139,45 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
|||||||
if err := migrateV21ToV22(); err != nil {
|
if err := migrateV21ToV22(); err != nil {
|
||||||
t.Fatal("migrate v21 to v22 should succeed when no config file is found")
|
t.Fatal("migrate v21 to v22 should succeed when no config file is found")
|
||||||
}
|
}
|
||||||
|
if err := migrateV22ToV23(); err != nil {
|
||||||
|
t.Fatal("migrate v22 to v23 should succeed when no config file is found")
|
||||||
|
}
|
||||||
|
if err := migrateV23ToV24(); err != nil {
|
||||||
|
t.Fatal("migrate v23 to v24 should succeed when no config file is found")
|
||||||
|
}
|
||||||
|
if err := migrateV24ToV25(); err != nil {
|
||||||
|
t.Fatal("migrate v24 to v25 should succeed when no config file is found")
|
||||||
|
}
|
||||||
|
if err := migrateV25ToV26(); err != nil {
|
||||||
|
t.Fatal("migrate v25 to v26 should succeed when no config file is found")
|
||||||
|
}
|
||||||
|
if err := migrateV26ToV27(); err != nil {
|
||||||
|
t.Fatal("migrate v26 to v27 should succeed when no config file is found")
|
||||||
|
}
|
||||||
|
if err := migrateV27ToV28(); err != nil {
|
||||||
|
t.Fatal("migrate v27 to v28 should succeed when no config file is found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if a config migration from v2 to v23 is successfully done
|
// Test if a config migration from v2 to v28 is successfully done
|
||||||
func TestServerConfigMigrateV2toV23(t *testing.T) {
|
func TestServerConfigMigrateV2toV28(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Init Test config failed")
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
defer os.RemoveAll(rootPath)
|
||||||
|
|
||||||
setConfigDir(rootPath)
|
setConfigDir(rootPath)
|
||||||
|
|
||||||
|
objLayer, fsDir, err := prepareFS()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(fsDir)
|
||||||
|
|
||||||
|
globalObjLayerMutex.Lock()
|
||||||
|
globalObjectAPI = objLayer
|
||||||
|
globalObjLayerMutex.Unlock()
|
||||||
|
|
||||||
configPath := rootPath + "/" + minioConfigFile
|
configPath := rootPath + "/" + minioConfigFile
|
||||||
|
|
||||||
// Create a corrupted config file
|
// Create a corrupted config file
|
||||||
@@ -171,8 +203,12 @@ func TestServerConfigMigrateV2toV23(t *testing.T) {
|
|||||||
t.Fatal("Unexpected error: ", err)
|
t.Fatal("Unexpected error: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := migrateConfigToMinioSys(); err != nil {
|
||||||
|
t.Fatal("Unexpected error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize server config and check again if everything is fine
|
// Initialize server config and check again if everything is fine
|
||||||
if err := loadConfig(); err != nil {
|
if err := loadConfig(newObjectLayerFn()); err != nil {
|
||||||
t.Fatalf("Unable to initialize from updated config file %s", err)
|
t.Fatalf("Unable to initialize from updated config file %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,13 +229,11 @@ func TestServerConfigMigrateV2toV23(t *testing.T) {
|
|||||||
|
|
||||||
// Test if all migrate code returns error with corrupted config files
|
// Test if all migrate code returns error with corrupted config files
|
||||||
func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Init Test config failed")
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
defer os.RemoveAll(rootPath)
|
||||||
|
|
||||||
setConfigDir(rootPath)
|
setConfigDir(rootPath)
|
||||||
configPath := rootPath + "/" + minioConfigFile
|
configPath := rootPath + "/" + minioConfigFile
|
||||||
|
|
||||||
@@ -272,17 +306,31 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
|||||||
if err := migrateV22ToV23(); err == nil {
|
if err := migrateV22ToV23(); err == nil {
|
||||||
t.Fatal("migrateConfigV22ToV23() should fail with a corrupted json")
|
t.Fatal("migrateConfigV22ToV23() should fail with a corrupted json")
|
||||||
}
|
}
|
||||||
|
if err := migrateV23ToV24(); err == nil {
|
||||||
|
t.Fatal("migrateConfigV23ToV24() should fail with a corrupted json")
|
||||||
|
}
|
||||||
|
if err := migrateV24ToV25(); err == nil {
|
||||||
|
t.Fatal("migrateConfigV24ToV25() should fail with a corrupted json")
|
||||||
|
}
|
||||||
|
if err := migrateV25ToV26(); err == nil {
|
||||||
|
t.Fatal("migrateConfigV25ToV26() should fail with a corrupted json")
|
||||||
|
}
|
||||||
|
if err := migrateV26ToV27(); err == nil {
|
||||||
|
t.Fatal("migrateConfigV26ToV27() should fail with a corrupted json")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migrateV27ToV28(); err == nil {
|
||||||
|
t.Fatal("migrateConfigV27ToV28() should fail with a corrupted json")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if all migrate code returns error with corrupted config files
|
// Test if all migrate code returns error with corrupted config files
|
||||||
func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
|
func TestServerConfigMigrateCorruptedConfig(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
rootPath, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Init Test config failed")
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// remove the root directory after the test ends.
|
|
||||||
defer os.RemoveAll(rootPath)
|
defer os.RemoveAll(rootPath)
|
||||||
|
|
||||||
setConfigDir(rootPath)
|
setConfigDir(rootPath)
|
||||||
configPath := rootPath + "/" + minioConfigFile
|
configPath := rootPath + "/" + minioConfigFile
|
||||||
|
|
||||||
|
|||||||
+78
-3
@@ -19,6 +19,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/event/target"
|
"github.com/minio/minio/pkg/event/target"
|
||||||
"github.com/minio/minio/pkg/quick"
|
"github.com/minio/minio/pkg/quick"
|
||||||
@@ -657,9 +658,6 @@ type serverConfigV25 struct {
|
|||||||
|
|
||||||
// serverConfigV26 is just like version '25', stores additionally
|
// serverConfigV26 is just like version '25', stores additionally
|
||||||
// cache max use value in 'CacheConfig'.
|
// cache max use value in 'CacheConfig'.
|
||||||
//
|
|
||||||
// IMPORTANT NOTE: When updating this struct make sure that
|
|
||||||
// serverConfig.ConfigDiff() is updated as necessary.
|
|
||||||
type serverConfigV26 struct {
|
type serverConfigV26 struct {
|
||||||
quick.Config `json:"-"` // ignore interfaces
|
quick.Config `json:"-"` // ignore interfaces
|
||||||
|
|
||||||
@@ -681,3 +679,80 @@ type serverConfigV26 struct {
|
|||||||
// Notification queue configuration.
|
// Notification queue configuration.
|
||||||
Notify notifier `json:"notify"`
|
Notify notifier `json:"notify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type loggerConsole struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggerHTTP struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggerConfig struct {
|
||||||
|
Console loggerConsole `json:"console"`
|
||||||
|
HTTP map[string]loggerHTTP `json:"http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverConfigV27 is just like version '26', stores additionally
|
||||||
|
// the logger field
|
||||||
|
//
|
||||||
|
// IMPORTANT NOTE: When updating this struct make sure that
|
||||||
|
// serverConfig.ConfigDiff() is updated as necessary.
|
||||||
|
type serverConfigV27 struct {
|
||||||
|
quick.Config `json:"-"` // ignore interfaces
|
||||||
|
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// S3 API configuration.
|
||||||
|
Credential auth.Credentials `json:"credential"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Browser BoolFlag `json:"browser"`
|
||||||
|
Worm BoolFlag `json:"worm"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
|
||||||
|
// Storage class configuration
|
||||||
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
|
|
||||||
|
// Cache configuration
|
||||||
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
|
// Notification queue configuration.
|
||||||
|
Notify notifier `json:"notify"`
|
||||||
|
|
||||||
|
// Logger configuration
|
||||||
|
Logger loggerConfig `json:"logger"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverConfigV28 is just like version '27', additionally
|
||||||
|
// storing KMS config
|
||||||
|
//
|
||||||
|
// IMPORTANT NOTE: When updating this struct make sure that
|
||||||
|
// serverConfig.ConfigDiff() is updated as necessary.
|
||||||
|
type serverConfigV28 struct {
|
||||||
|
quick.Config `json:"-"` // ignore interfaces
|
||||||
|
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// S3 API configuration.
|
||||||
|
Credential auth.Credentials `json:"credential"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Browser BoolFlag `json:"browser"`
|
||||||
|
Worm BoolFlag `json:"worm"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
|
||||||
|
// Storage class configuration
|
||||||
|
StorageClass storageClassConfig `json:"storageclass"`
|
||||||
|
|
||||||
|
// Cache configuration
|
||||||
|
Cache CacheConfig `json:"cache"`
|
||||||
|
|
||||||
|
// KMS configuration
|
||||||
|
KMS crypto.KMSConfig `json:"kms"`
|
||||||
|
|
||||||
|
// Notification queue configuration.
|
||||||
|
Notify notifier `json:"notify"`
|
||||||
|
|
||||||
|
// Logger configuration
|
||||||
|
Logger loggerConfig `json:"logger"`
|
||||||
|
}
|
||||||
|
|||||||
+267
@@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage, (C) 2018 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
"github.com/minio/minio/pkg/quick"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minioConfigPrefix = "config"
|
||||||
|
|
||||||
|
// Minio configuration file.
|
||||||
|
minioConfigFile = "config.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func saveServerConfig(objAPI ObjectLayer, config *serverConfig) error {
|
||||||
|
if err := quick.CheckData(config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
_, err := globalEtcdClient.Put(ctx, configFile, string(data))
|
||||||
|
defer cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveConfig(objAPI, configFile, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigEtcd(configFile string) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
resp, err := globalEtcdClient.Get(ctx, configFile)
|
||||||
|
defer cancel()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.Count == 0 {
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
for _, ev := range resp.Kvs {
|
||||||
|
if string(ev.Key) == configFile {
|
||||||
|
return ev.Value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func readServerConfig(ctx context.Context, objAPI ObjectLayer) (*serverConfig, error) {
|
||||||
|
var configData []byte
|
||||||
|
var err error
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
configData, err = readConfigEtcd(configFile)
|
||||||
|
} else {
|
||||||
|
var reader io.Reader
|
||||||
|
reader, err = readConfig(ctx, objAPI, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
configData, err = ioutil.ReadAll(reader)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
configData = bytes.Replace(configData, []byte("\r\n"), []byte("\n"), -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = quick.CheckDuplicateKeys(string(configData)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = &serverConfig{}
|
||||||
|
if err := json.Unmarshal(configData, config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := quick.CheckData(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkServerConfigEtcd(configFile string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
resp, err := globalEtcdClient.Get(ctx, configFile)
|
||||||
|
defer cancel()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.Count == 0 {
|
||||||
|
return errConfigNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkServerConfig(ctx context.Context, objAPI ObjectLayer) error {
|
||||||
|
configFile := path.Join(minioConfigPrefix, minioConfigFile)
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
return checkServerConfigEtcd(configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := objAPI.GetObjectInfo(ctx, minioMetaBucket, configFile); err != nil {
|
||||||
|
if isErrObjectNotFound(err) {
|
||||||
|
return errConfigNotFound
|
||||||
|
}
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfig(objAPI ObjectLayer, configFile string, data []byte) error {
|
||||||
|
hashReader, err := hash.NewReader(bytes.NewReader(data), int64(len(data)), "", getSHA256Hash(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = objAPI.PutObject(context.Background(), minioMetaBucket, configFile, hashReader, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var errConfigNotFound = errors.New("config file not found")
|
||||||
|
|
||||||
|
func readConfig(ctx context.Context, objAPI ObjectLayer, configFile string) (*bytes.Buffer, error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
// Read entire content by setting size to -1
|
||||||
|
if err := objAPI.GetObject(ctx, minioMetaBucket, configFile, 0, -1, &buffer, ""); err != nil {
|
||||||
|
// Ignore if err is ObjectNotFound or IncompleteBody when bucket is not configured with notification
|
||||||
|
if isErrObjectNotFound(err) || isErrIncompleteBody(err) || isInsufficientReadQuorum(err) {
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.GetReqInfo(ctx).AppendTags("configFile", configFile)
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return config not found on empty content.
|
||||||
|
if buffer.Len() == 0 {
|
||||||
|
return nil, errConfigNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buffer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigSys - config system.
|
||||||
|
type ConfigSys struct{}
|
||||||
|
|
||||||
|
// Load - load config.json.
|
||||||
|
func (sys *ConfigSys) Load(objAPI ObjectLayer) error {
|
||||||
|
return sys.Init(objAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init - initializes config system from config.json.
|
||||||
|
func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
|
||||||
|
if objAPI == nil {
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
|
return loadConfig(objAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigSys - creates new config system object.
|
||||||
|
func NewConfigSys() *ConfigSys {
|
||||||
|
return &ConfigSys{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/minio.json'
|
||||||
|
func migrateConfigToMinioSys() error {
|
||||||
|
// Construct path to config.json for the given bucket.
|
||||||
|
configFile := path.Join(bucketConfigPrefix, minioConfigFile)
|
||||||
|
transactionConfigFile := configFile + ".transaction"
|
||||||
|
|
||||||
|
// As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket
|
||||||
|
// and configFile, take a transaction lock to avoid race.
|
||||||
|
objLock := globalNSMutex.NewNSLock(minioMetaBucket, transactionConfigFile)
|
||||||
|
if err := objLock.GetLock(globalOperationTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer objLock.Unlock()
|
||||||
|
|
||||||
|
// Verify if backend already has the file.
|
||||||
|
if err := checkServerConfig(context.Background(), newObjectLayerFn()); err != errConfigNotFound {
|
||||||
|
return err
|
||||||
|
} // if errConfigNotFound proceed to migrate..
|
||||||
|
|
||||||
|
var config = &serverConfig{}
|
||||||
|
if _, err := Load(getConfigFile(), config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveServerConfig(newObjectLayerFn(), config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize and load config from remote etcd or local config directory
|
||||||
|
func initConfig() {
|
||||||
|
if globalEtcdClient != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
||||||
|
cancel()
|
||||||
|
if err == nil && resp.Count > 0 {
|
||||||
|
logger.FatalIf(migrateConfig(), "Config migration failed")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if isFile(getConfigFile()) {
|
||||||
|
logger.FatalIf(migrateConfig(), "Config migration failed")
|
||||||
|
|
||||||
|
// Migrates ${HOME}/.minio/config.json to '<export_path>/.minio.sys/config/config.json'
|
||||||
|
if err := migrateConfigToMinioSys(); err != nil {
|
||||||
|
logger.Fatal(err, "Unable to migrate 'config.json' to '.minio.sys/config/config.json'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
objAPI := newObjectLayerFn()
|
||||||
|
if objAPI == nil {
|
||||||
|
logger.FatalIf(errServerNotInitialized, "Server is not initialized yet unable to proceed")
|
||||||
|
}
|
||||||
|
if err := checkServerConfig(context.Background(), objAPI); err != nil {
|
||||||
|
if err == errConfigNotFound {
|
||||||
|
// Config file does not exist, we create it fresh and return upon success.
|
||||||
|
logger.FatalIf(newConfig(objAPI), "Unable to initialize minio config for the first time")
|
||||||
|
logger.Info("Created minio configuration file successfully at " + getConfigDir())
|
||||||
|
} else {
|
||||||
|
logger.FatalIf(err, "Unable to load the configuration file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FatalIf(migrateMinioSysConfig(objAPI), "Config migration failed for minio.sys config")
|
||||||
|
|
||||||
|
logger.FatalIf(loadConfig(objAPI), "Unable to load the configuration file")
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
// KMSConfig has the KMS config for hashicorp vault
|
||||||
|
type KMSConfig struct {
|
||||||
|
Vault VaultConfig `json:"vault"`
|
||||||
|
}
|
||||||
+6
-6
@@ -32,7 +32,7 @@
|
|||||||
// Input: ClientKey, bucket, object, metadata, object_data
|
// Input: ClientKey, bucket, object, metadata, object_data
|
||||||
// - IV := Random({0,1}²⁵⁶)
|
// - IV := Random({0,1}²⁵⁶)
|
||||||
// - ObjectKey := SHA256(ClientKey || Random({0,1}²⁵⁶))
|
// - ObjectKey := SHA256(ClientKey || Random({0,1}²⁵⁶))
|
||||||
// - KeyEncKey := HMAC-SHA256(ClientKey, IV || bucket || object)
|
// - KeyEncKey := HMAC-SHA256(ClientKey, IV || 'SSE-C' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||||
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
|
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
|
||||||
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
|
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
|
||||||
// - metadata <- IV
|
// - metadata <- IV
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
// Input: ClientKey, bucket, object, metadata, enc_object_data
|
// Input: ClientKey, bucket, object, metadata, enc_object_data
|
||||||
// - IV <- metadata
|
// - IV <- metadata
|
||||||
// - SealedKey <- metadata
|
// - SealedKey <- metadata
|
||||||
// - KeyEncKey := HMAC-SHA256(ClientKey, IV || bucket || object)
|
// - KeyEncKey := HMAC-SHA256(ClientKey, IV || 'SSE-C' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||||
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
|
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
|
||||||
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
|
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
|
||||||
// Output: object_data
|
// Output: object_data
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
// Input: MasterKey, bucket, object, metadata, object_data
|
// Input: MasterKey, bucket, object, metadata, object_data
|
||||||
// - IV := Random({0,1}²⁵⁶)
|
// - IV := Random({0,1}²⁵⁶)
|
||||||
// - ObjectKey := SHA256(MasterKey || Random({0,1}²⁵⁶))
|
// - ObjectKey := SHA256(MasterKey || Random({0,1}²⁵⁶))
|
||||||
// - KeyEncKey := HMAC-SHA256(MasterKey, IV || bucket || object)
|
// - KeyEncKey := HMAC-SHA256(MasterKey, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||||
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
|
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
|
||||||
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
|
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
|
||||||
// - metadata <- IV
|
// - metadata <- IV
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
// Input: MasterKey, bucket, object, metadata, enc_object_data
|
// Input: MasterKey, bucket, object, metadata, enc_object_data
|
||||||
// - IV <- metadata
|
// - IV <- metadata
|
||||||
// - SealedKey <- metadata
|
// - SealedKey <- metadata
|
||||||
// - KeyEncKey := HMAC-SHA256(MasterKey, IV || bucket || object)
|
// - KeyEncKey := HMAC-SHA256(MasterKey, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||||
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
|
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
|
||||||
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
|
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
|
||||||
// Output: object_data
|
// Output: object_data
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
// - Key, EncKey := Generate(KeyID)
|
// - Key, EncKey := Generate(KeyID)
|
||||||
// - IV := Random({0,1}²⁵⁶)
|
// - IV := Random({0,1}²⁵⁶)
|
||||||
// - ObjectKey := SHA256(Key, Random({0,1}²⁵⁶))
|
// - ObjectKey := SHA256(Key, Random({0,1}²⁵⁶))
|
||||||
// - KeyEncKey := HMAC-SHA256(Key, IV || bucket || object)
|
// - KeyEncKey := HMAC-SHA256(Key, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||||
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
|
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
|
||||||
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
|
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
|
||||||
// - metadata <- IV
|
// - metadata <- IV
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
// - IV <- metadata
|
// - IV <- metadata
|
||||||
// - SealedKey <- metadata
|
// - SealedKey <- metadata
|
||||||
// - Key := Unseal(KeyID, EncKey)
|
// - Key := Unseal(KeyID, EncKey)
|
||||||
// - KeyEncKey := HMAC-SHA256(Key, IV || bucket || object)
|
// - KeyEncKey := HMAC-SHA256(Key, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||||
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
|
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
|
||||||
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
|
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
|
||||||
// Output: object_data
|
// Output: object_data
|
||||||
|
|||||||
@@ -16,8 +16,52 @@ package crypto
|
|||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
|
// Error is the generic type for any error happening during decrypting
|
||||||
|
// an object. It indicates that the object itself or its metadata was
|
||||||
|
// modified accidentally or maliciously.
|
||||||
|
type Error struct{ msg string }
|
||||||
|
|
||||||
|
func (e Error) Error() string { return e.msg }
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrInvalidEncryptionMethod indicates that the specified SSE encryption method
|
// ErrInvalidEncryptionMethod indicates that the specified SSE encryption method
|
||||||
// is not supported.
|
// is not supported.
|
||||||
ErrInvalidEncryptionMethod = errors.New("The encryption method is not supported")
|
ErrInvalidEncryptionMethod = errors.New("The encryption method is not supported")
|
||||||
|
|
||||||
|
// ErrInvalidCustomerAlgorithm indicates that the specified SSE-C algorithm
|
||||||
|
// is not supported.
|
||||||
|
ErrInvalidCustomerAlgorithm = errors.New("The SSE-C algorithm is not supported")
|
||||||
|
|
||||||
|
// ErrMissingCustomerKey indicates that the HTTP headers contains no SSE-C client key.
|
||||||
|
ErrMissingCustomerKey = errors.New("The SSE-C request is missing the customer key")
|
||||||
|
|
||||||
|
// ErrMissingCustomerKeyMD5 indicates that the HTTP headers contains no SSE-C client key
|
||||||
|
// MD5 checksum.
|
||||||
|
ErrMissingCustomerKeyMD5 = errors.New("The SSE-C request is missing the customer key MD5")
|
||||||
|
|
||||||
|
// ErrInvalidCustomerKey indicates that the SSE-C client key is not valid - e.g. not a
|
||||||
|
// base64-encoded string or not 256 bits long.
|
||||||
|
ErrInvalidCustomerKey = errors.New("The SSE-C client key is invalid")
|
||||||
|
|
||||||
|
// ErrCustomerKeyMD5Mismatch indicates that the SSE-C key MD5 does not match the
|
||||||
|
// computed MD5 sum. This means that the client provided either the wrong key for
|
||||||
|
// a certain MD5 checksum or the wrong MD5 for a certain key.
|
||||||
|
ErrCustomerKeyMD5Mismatch = errors.New("The provided SSE-C key MD5 does not match the computed MD5 of the SSE-C key")
|
||||||
|
// ErrIncompatibleEncryptionMethod indicates that both SSE-C headers and SSE-S3 headers were specified, and are incompatible
|
||||||
|
// The client needs to remove the SSE-S3 header or the SSE-C headers
|
||||||
|
ErrIncompatibleEncryptionMethod = errors.New("Server side encryption specified with both SSE-C and SSE-S3 headers")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMissingInternalIV = Error{"The object metadata is missing the internal encryption IV"}
|
||||||
|
errMissingInternalSealAlgorithm = Error{"The object metadata is missing the internal seal algorithm"}
|
||||||
|
|
||||||
|
errInvalidInternalIV = Error{"The internal encryption IV is malformed"}
|
||||||
|
errInvalidInternalSealAlgorithm = Error{"The internal seal algorithm is invalid and not supported"}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// errOutOfEntropy indicates that the a source of randomness (PRNG) wasn't able
|
||||||
|
// to produce enough random data. This is fatal error and should cause a panic.
|
||||||
|
errOutOfEntropy = errors.New("Unable to read enough randomness from the system")
|
||||||
)
|
)
|
||||||
|
|||||||
+172
-7
@@ -15,16 +15,64 @@
|
|||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SSEHeader is the general AWS SSE HTTP header key.
|
// SSEHeader is the general AWS SSE HTTP header key.
|
||||||
const SSEHeader = "X-Amz-Server-Side-Encryption"
|
const SSEHeader = "X-Amz-Server-Side-Encryption"
|
||||||
|
|
||||||
// SSEAlgorithmAES256 is the only supported value for the SSE-S3 or SSE-C algorithm header.
|
const (
|
||||||
// For SSE-S3 see: https://docs.aws.amazon.com/AmazonS3/latest/dev/SSEUsingRESTAPI.html
|
// SSEKmsID is the HTTP header key referencing the SSE-KMS
|
||||||
// For SSE-C see: https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html
|
// key ID.
|
||||||
const SSEAlgorithmAES256 = "AES256"
|
SSEKmsID = SSEHeader + "-Aws-Kms-Key-Id"
|
||||||
|
|
||||||
|
// SSEKmsContext is the HTTP header key referencing the
|
||||||
|
// SSE-KMS encryption context.
|
||||||
|
SSEKmsContext = SSEHeader + "-Context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SSECAlgorithm is the HTTP header key referencing
|
||||||
|
// the SSE-C algorithm.
|
||||||
|
SSECAlgorithm = SSEHeader + "-Customer-Algorithm"
|
||||||
|
|
||||||
|
// SSECKey is the HTTP header key referencing the
|
||||||
|
// SSE-C client-provided key..
|
||||||
|
SSECKey = SSEHeader + "-Customer-Key"
|
||||||
|
|
||||||
|
// SSECKeyMD5 is the HTTP header key referencing
|
||||||
|
// the MD5 sum of the client-provided key.
|
||||||
|
SSECKeyMD5 = SSEHeader + "-Customer-Key-Md5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SSECopyAlgorithm is the HTTP header key referencing
|
||||||
|
// the SSE-C algorithm for SSE-C copy requests.
|
||||||
|
SSECopyAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm"
|
||||||
|
|
||||||
|
// SSECopyKey is the HTTP header key referencing the SSE-C
|
||||||
|
// client-provided key for SSE-C copy requests.
|
||||||
|
SSECopyKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"
|
||||||
|
|
||||||
|
// SSECopyKeyMD5 is the HTTP header key referencing the
|
||||||
|
// MD5 sum of the client key for SSE-C copy requests.
|
||||||
|
SSECopyKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SSEAlgorithmAES256 is the only supported value for the SSE-S3 or SSE-C algorithm header.
|
||||||
|
// For SSE-S3 see: https://docs.aws.amazon.com/AmazonS3/latest/dev/SSEUsingRESTAPI.html
|
||||||
|
// For SSE-C see: https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html
|
||||||
|
SSEAlgorithmAES256 = "AES256"
|
||||||
|
|
||||||
|
// SSEAlgorithmKMS is the value of 'X-Amz-Server-Side-Encryption' for SSE-KMS.
|
||||||
|
// See: https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html
|
||||||
|
SSEAlgorithmKMS = "aws:kms"
|
||||||
|
)
|
||||||
|
|
||||||
// S3 represents AWS SSE-S3. It provides functionality to handle
|
// S3 represents AWS SSE-S3. It provides functionality to handle
|
||||||
// SSE-S3 requests.
|
// SSE-S3 requests.
|
||||||
@@ -36,14 +84,131 @@ type s3 struct{}
|
|||||||
// the S3 client requests SSE-S3.
|
// the S3 client requests SSE-S3.
|
||||||
func (s3) IsRequested(h http.Header) bool {
|
func (s3) IsRequested(h http.Header) bool {
|
||||||
_, ok := h[SSEHeader]
|
_, ok := h[SSEHeader]
|
||||||
return ok
|
return ok && strings.ToLower(h.Get(SSEHeader)) != SSEAlgorithmKMS // Return only true if the SSE header is specified and does not contain the SSE-KMS value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse parses the SSE-S3 related HTTP headers and checks
|
// ParseHTTP parses the SSE-S3 related HTTP headers and checks
|
||||||
// whether they contain valid values.
|
// whether they contain valid values.
|
||||||
func (s3) Parse(h http.Header) (err error) {
|
func (s3) ParseHTTP(h http.Header) (err error) {
|
||||||
if h.Get(SSEHeader) != SSEAlgorithmAES256 {
|
if h.Get(SSEHeader) != SSEAlgorithmAES256 {
|
||||||
err = ErrInvalidEncryptionMethod
|
err = ErrInvalidEncryptionMethod
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// S3KMS represents AWS SSE-KMS. It provides functionality to
|
||||||
|
// handle SSE-KMS requests.
|
||||||
|
var S3KMS = s3KMS{}
|
||||||
|
|
||||||
|
type s3KMS struct{}
|
||||||
|
|
||||||
|
// IsRequested returns true if the HTTP headers indicates that
|
||||||
|
// the S3 client requests SSE-KMS.
|
||||||
|
func (s3KMS) IsRequested(h http.Header) bool {
|
||||||
|
if _, ok := h[SSEKmsID]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := h[SSEKmsContext]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := h[SSEHeader]; ok {
|
||||||
|
return strings.ToUpper(h.Get(SSEHeader)) != SSEAlgorithmAES256 // Return only true if the SSE header is specified and does not contain the SSE-S3 value
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// SSEC represents AWS SSE-C. It provides functionality to handle
|
||||||
|
// SSE-C requests.
|
||||||
|
SSEC = ssec{}
|
||||||
|
|
||||||
|
// SSECopy represents AWS SSE-C for copy requests. It provides
|
||||||
|
// functionality to handle SSE-C copy requests.
|
||||||
|
SSECopy = ssecCopy{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ssec struct{}
|
||||||
|
type ssecCopy struct{}
|
||||||
|
|
||||||
|
// IsRequested returns true if the HTTP headers contains
|
||||||
|
// at least one SSE-C header. SSE-C copy headers are ignored.
|
||||||
|
func (ssec) IsRequested(h http.Header) bool {
|
||||||
|
if _, ok := h[SSECAlgorithm]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := h[SSECKey]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := h[SSECKeyMD5]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRequested returns true if the HTTP headers contains
|
||||||
|
// at least one SSE-C copy header. Regular SSE-C headers
|
||||||
|
// are ignored.
|
||||||
|
func (ssecCopy) IsRequested(h http.Header) bool {
|
||||||
|
if _, ok := h[SSECopyAlgorithm]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := h[SSECopyKey]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := h[SSECopyKeyMD5]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseHTTP parses the SSE-C headers and returns the SSE-C client key
|
||||||
|
// on success. SSE-C copy headers are ignored.
|
||||||
|
func (ssec) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
||||||
|
defer h.Del(SSECKey) // remove SSE-C key from headers after parsing
|
||||||
|
if h.Get(SSECAlgorithm) != SSEAlgorithmAES256 {
|
||||||
|
return key, ErrInvalidCustomerAlgorithm
|
||||||
|
}
|
||||||
|
if h.Get(SSECKey) == "" {
|
||||||
|
return key, ErrMissingCustomerKey
|
||||||
|
}
|
||||||
|
if h.Get(SSECKeyMD5) == "" {
|
||||||
|
return key, ErrMissingCustomerKeyMD5
|
||||||
|
}
|
||||||
|
|
||||||
|
clientKey, err := base64.StdEncoding.DecodeString(h.Get(SSECKey))
|
||||||
|
if err != nil || len(clientKey) != 32 { // The client key must be 256 bits long
|
||||||
|
return key, ErrInvalidCustomerKey
|
||||||
|
}
|
||||||
|
keyMD5, err := base64.StdEncoding.DecodeString(h.Get(SSECKeyMD5))
|
||||||
|
if md5Sum := md5.Sum(clientKey); err != nil || !bytes.Equal(md5Sum[:], keyMD5) {
|
||||||
|
return key, ErrCustomerKeyMD5Mismatch
|
||||||
|
}
|
||||||
|
copy(key[:], clientKey)
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseHTTP parses the SSE-C copy headers and returns the SSE-C client key
|
||||||
|
// on success. Regular SSE-C headers are ignored.
|
||||||
|
func (ssecCopy) ParseHTTP(h http.Header) (key [32]byte, err error) {
|
||||||
|
defer h.Del(SSECopyKey) // remove SSE-C copy key of source object from headers after parsing
|
||||||
|
if h.Get(SSECopyAlgorithm) != SSEAlgorithmAES256 {
|
||||||
|
return key, ErrInvalidCustomerAlgorithm
|
||||||
|
}
|
||||||
|
if h.Get(SSECopyKey) == "" {
|
||||||
|
return key, ErrMissingCustomerKey
|
||||||
|
}
|
||||||
|
if h.Get(SSECopyKeyMD5) == "" {
|
||||||
|
return key, ErrMissingCustomerKeyMD5
|
||||||
|
}
|
||||||
|
|
||||||
|
clientKey, err := base64.StdEncoding.DecodeString(h.Get(SSECopyKey))
|
||||||
|
if err != nil || len(clientKey) != 32 { // The client key must be 256 bits long
|
||||||
|
return key, ErrInvalidCustomerKey
|
||||||
|
}
|
||||||
|
keyMD5, err := base64.StdEncoding.DecodeString(h.Get(SSECopyKeyMD5))
|
||||||
|
if md5Sum := md5.Sum(clientKey); err != nil || !bytes.Equal(md5Sum[:], keyMD5) {
|
||||||
|
return key, ErrCustomerKeyMD5Mismatch
|
||||||
|
}
|
||||||
|
copy(key[:], clientKey)
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|||||||
+292
-9
@@ -19,25 +19,60 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var isRequestedTests = []struct {
|
var kmsIsRequestedTests = []struct {
|
||||||
Header http.Header
|
Header http.Header
|
||||||
Expected bool
|
Expected bool
|
||||||
}{
|
}{
|
||||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, Expected: true}, // 0
|
{Header: http.Header{}, Expected: false}, // 0
|
||||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES-256"}}, Expected: true}, // 1
|
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"aws:kms"}}, Expected: true}, // 1
|
||||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{""}}, Expected: true}, // 2
|
{Header: http.Header{"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": []string{"0839-9047947-844842874-481"}}, Expected: true}, // 2
|
||||||
{Header: http.Header{"X-Amz-Server-Side-Encryptio": []string{"AES256"}}, Expected: false}, // 3
|
{Header: http.Header{"X-Amz-Server-Side-Encryption-Context": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption": []string{""},
|
||||||
|
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": []string{""},
|
||||||
|
"X-Amz-Server-Side-Encryption-Context": []string{""},
|
||||||
|
},
|
||||||
|
Expected: true,
|
||||||
|
}, // 4
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": []string{""},
|
||||||
|
},
|
||||||
|
Expected: true,
|
||||||
|
}, // 5
|
||||||
|
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, Expected: false}, // 6
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKMSIsRequested(t *testing.T) {
|
||||||
|
for i, test := range kmsIsRequestedTests {
|
||||||
|
if got := S3KMS.IsRequested(test.Header); got != test.Expected {
|
||||||
|
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var s3IsRequestedTests = []struct {
|
||||||
|
Header http.Header
|
||||||
|
Expected bool
|
||||||
|
}{
|
||||||
|
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, Expected: true}, // 0
|
||||||
|
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES-256"}}, Expected: true}, // 1
|
||||||
|
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{""}}, Expected: true}, // 2
|
||||||
|
{Header: http.Header{"X-Amz-Server-Side-Encryptio": []string{"AES256"}}, Expected: false}, // 3
|
||||||
|
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{SSEAlgorithmKMS}}, Expected: false}, // 4
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestS3IsRequested(t *testing.T) {
|
func TestS3IsRequested(t *testing.T) {
|
||||||
for i, test := range isRequestedTests {
|
for i, test := range s3IsRequestedTests {
|
||||||
if got := S3.IsRequested(test.Header); got != test.Expected {
|
if got := S3.IsRequested(test.Header); got != test.Expected {
|
||||||
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
|
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var parseTests = []struct {
|
var s3ParseTests = []struct {
|
||||||
Header http.Header
|
Header http.Header
|
||||||
ExpectedErr error
|
ExpectedErr error
|
||||||
}{
|
}{
|
||||||
@@ -48,9 +83,257 @@ var parseTests = []struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestS3Parse(t *testing.T) {
|
func TestS3Parse(t *testing.T) {
|
||||||
for i, test := range parseTests {
|
for i, test := range s3ParseTests {
|
||||||
if err := S3.Parse(test.Header); err != test.ExpectedErr {
|
if err := S3.ParseHTTP(test.Header); err != test.ExpectedErr {
|
||||||
t.Errorf("Test %d: Wanted '%v' but got '%v'", i, test.ExpectedErr, err)
|
t.Errorf("Test %d: Wanted '%v' but got '%v'", i, test.ExpectedErr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ssecIsRequestedTests = []struct {
|
||||||
|
Header http.Header
|
||||||
|
Expected bool
|
||||||
|
}{
|
||||||
|
{Header: http.Header{}, Expected: false}, // 0
|
||||||
|
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1
|
||||||
|
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2
|
||||||
|
{Header: http.Header{"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{""},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{""},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{""},
|
||||||
|
},
|
||||||
|
Expected: true,
|
||||||
|
}, // 4
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Expected: true,
|
||||||
|
}, // 5
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Expected: false,
|
||||||
|
}, // 6
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECIsRequested(t *testing.T) {
|
||||||
|
for i, test := range ssecIsRequestedTests {
|
||||||
|
if got := SSEC.IsRequested(test.Header); got != test.Expected {
|
||||||
|
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssecCopyIsRequestedTests = []struct {
|
||||||
|
Header http.Header
|
||||||
|
Expected bool
|
||||||
|
}{
|
||||||
|
{Header: http.Header{}, Expected: false}, // 0
|
||||||
|
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"}}, Expected: true}, // 1
|
||||||
|
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}}, Expected: true}, // 2
|
||||||
|
{Header: http.Header{"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="}}, Expected: true}, // 3
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{""},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{""},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{""},
|
||||||
|
},
|
||||||
|
Expected: true,
|
||||||
|
}, // 4
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Expected: true,
|
||||||
|
}, // 5
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
Expected: false,
|
||||||
|
}, // 6
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECopyIsRequested(t *testing.T) {
|
||||||
|
for i, test := range ssecCopyIsRequestedTests {
|
||||||
|
if got := SSECopy.IsRequested(test.Header); got != test.Expected {
|
||||||
|
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssecParseTests = []struct {
|
||||||
|
Header http.Header
|
||||||
|
ExpectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: nil, // 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES-256"}, // invalid algorithm
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrInvalidCustomerAlgorithm, // 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{""}, // no client key
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrMissingCustomerKey, // 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRr.ZXltdXN0cHJvdmlkZWQ="}, // invalid key
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrInvalidCustomerKey, // 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{""}, // no key MD5
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrMissingCustomerKeyMD5, // 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"DzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, // wrong client key
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrCustomerKeyMD5Mismatch, // 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": []string{".7PpPLAK26ONlVUGOWlusfg=="}, // wrong key MD5
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrCustomerKeyMD5Mismatch, // 6
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECParse(t *testing.T) {
|
||||||
|
var zeroKey [32]byte
|
||||||
|
for i, test := range ssecParseTests {
|
||||||
|
key, err := SSEC.ParseHTTP(test.Header)
|
||||||
|
if err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: want error '%v' but got '%v'", i, test.ExpectedErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && key != zeroKey {
|
||||||
|
t.Errorf("Test %d: parsing failed and client key is not zero key", i)
|
||||||
|
}
|
||||||
|
if err == nil && key == zeroKey {
|
||||||
|
t.Errorf("Test %d: parsed client key is zero key", i)
|
||||||
|
}
|
||||||
|
if _, ok := test.Header[SSECKey]; ok {
|
||||||
|
t.Errorf("Test %d: client key is not removed from HTTP headers after parsing", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssecCopyParseTests = []struct {
|
||||||
|
Header http.Header
|
||||||
|
ExpectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: nil, // 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES-256"}, // invalid algorithm
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrInvalidCustomerAlgorithm, // 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{""}, // no client key
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrMissingCustomerKey, // 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRr.ZXltdXN0cHJvdmlkZWQ="}, // invalid key
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrInvalidCustomerKey, // 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{""}, // no key MD5
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrMissingCustomerKeyMD5, // 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"DzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="}, // wrong client key
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{"7PpPLAK26ONlVUGOWlusfg=="},
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrCustomerKeyMD5Mismatch, // 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: http.Header{
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": []string{"AES256"},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": []string{"MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ="},
|
||||||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": []string{".7PpPLAK26ONlVUGOWlusfg=="}, // wrong key MD5
|
||||||
|
},
|
||||||
|
ExpectedErr: ErrCustomerKeyMD5Mismatch, // 6
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECopyParse(t *testing.T) {
|
||||||
|
var zeroKey [32]byte
|
||||||
|
for i, test := range ssecCopyParseTests {
|
||||||
|
key, err := SSECopy.ParseHTTP(test.Header)
|
||||||
|
if err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: want error '%v' but got '%v'", i, test.ExpectedErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && key != zeroKey {
|
||||||
|
t.Errorf("Test %d: parsing failed and client key is not zero key", i)
|
||||||
|
}
|
||||||
|
if err == nil && key == zeroKey {
|
||||||
|
t.Errorf("Test %d: parsed client key is zero key", i)
|
||||||
|
}
|
||||||
|
if _, ok := test.Header[SSECKey]; ok {
|
||||||
|
t.Errorf("Test %d: client key is not removed from HTTP headers after parsing", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+71
-22
@@ -21,8 +21,9 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
sha256 "github.com/minio/sha256-simd"
|
sha256 "github.com/minio/sha256-simd"
|
||||||
@@ -34,50 +35,98 @@ import (
|
|||||||
type ObjectKey [32]byte
|
type ObjectKey [32]byte
|
||||||
|
|
||||||
// GenerateKey generates a unique ObjectKey from a 256 bit external key
|
// GenerateKey generates a unique ObjectKey from a 256 bit external key
|
||||||
// and a source of randomness. If random is nil the default PRNG of system
|
// and a source of randomness. If random is nil the default PRNG of the
|
||||||
// (crypto/rand) is used.
|
// system (crypto/rand) is used.
|
||||||
func GenerateKey(extKey [32]byte, random io.Reader) (key ObjectKey) {
|
func GenerateKey(extKey [32]byte, random io.Reader) (key ObjectKey) {
|
||||||
if random == nil {
|
if random == nil {
|
||||||
random = rand.Reader
|
random = rand.Reader
|
||||||
}
|
}
|
||||||
var nonce [32]byte
|
var nonce [32]byte
|
||||||
if _, err := io.ReadFull(random, nonce[:]); err != nil {
|
if _, err := io.ReadFull(random, nonce[:]); err != nil {
|
||||||
logger.CriticalIf(context.Background(), errors.New("Unable to read enough randomness from the system"))
|
logger.CriticalIf(context.Background(), errOutOfEntropy)
|
||||||
}
|
}
|
||||||
sha := sha256.New()
|
sha := sha256.New()
|
||||||
sha.Write(extKey[:])
|
sha.Write(extKey[:])
|
||||||
sha.Write(nonce[:])
|
sha.Write(nonce[:])
|
||||||
sha.Sum(key[:0])
|
sha.Sum(key[:0])
|
||||||
return
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateIV generates a new random 256 bit IV from the provided source
|
||||||
|
// of randomness. If random is nil the default PRNG of the system
|
||||||
|
// (crypto/rand) is used.
|
||||||
|
func GenerateIV(random io.Reader) (iv [32]byte) {
|
||||||
|
if random == nil {
|
||||||
|
random = rand.Reader
|
||||||
|
}
|
||||||
|
if _, err := io.ReadFull(random, iv[:]); err != nil {
|
||||||
|
logger.CriticalIf(context.Background(), errOutOfEntropy)
|
||||||
|
}
|
||||||
|
return iv
|
||||||
|
}
|
||||||
|
|
||||||
|
// SealedKey represents a sealed object key. It can be stored
|
||||||
|
// at an untrusted location.
|
||||||
|
type SealedKey struct {
|
||||||
|
Key [64]byte // The encrypted and authenticted object-key.
|
||||||
|
IV [32]byte // The random IV used to encrypt the object-key.
|
||||||
|
Algorithm string // The sealing algorithm used to encrypt the object key.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seal encrypts the ObjectKey using the 256 bit external key and IV. The sealed
|
// Seal encrypts the ObjectKey using the 256 bit external key and IV. The sealed
|
||||||
// key is also cryptographically bound to the object's path (bucket/object).
|
// key is also cryptographically bound to the object's path (bucket/object) and the
|
||||||
func (key ObjectKey) Seal(extKey, iv [32]byte, bucket, object string) []byte {
|
// domain (SSE-C or SSE-S3).
|
||||||
var sealedKey bytes.Buffer
|
func (key ObjectKey) Seal(extKey, iv [32]byte, domain, bucket, object string) SealedKey {
|
||||||
|
var (
|
||||||
|
sealingKey [32]byte
|
||||||
|
encryptedKey bytes.Buffer
|
||||||
|
)
|
||||||
mac := hmac.New(sha256.New, extKey[:])
|
mac := hmac.New(sha256.New, extKey[:])
|
||||||
mac.Write(iv[:])
|
mac.Write(iv[:])
|
||||||
mac.Write([]byte(filepath.Join(bucket, object)))
|
mac.Write([]byte(domain))
|
||||||
|
mac.Write([]byte(SealAlgorithm))
|
||||||
if n, err := sio.Encrypt(&sealedKey, bytes.NewReader(key[:]), sio.Config{Key: mac.Sum(nil)}); n != 64 || err != nil {
|
mac.Write([]byte(path.Join(bucket, object))) // use path.Join for canonical 'bucket/object'
|
||||||
|
mac.Sum(sealingKey[:0])
|
||||||
|
if n, err := sio.Encrypt(&encryptedKey, bytes.NewReader(key[:]), sio.Config{Key: sealingKey[:]}); n != 64 || err != nil {
|
||||||
logger.CriticalIf(context.Background(), errors.New("Unable to generate sealed key"))
|
logger.CriticalIf(context.Background(), errors.New("Unable to generate sealed key"))
|
||||||
}
|
}
|
||||||
return sealedKey.Bytes()
|
sealedKey := SealedKey{
|
||||||
|
IV: iv,
|
||||||
|
Algorithm: SealAlgorithm,
|
||||||
|
}
|
||||||
|
copy(sealedKey.Key[:], encryptedKey.Bytes())
|
||||||
|
return sealedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unseal decrypts a sealed key using the 256 bit external key and IV. Since the sealed key
|
// Unseal decrypts a sealed key using the 256 bit external key. Since the sealed key
|
||||||
// is cryptographically bound to the object's path the same bucket/object as during sealing
|
// may be cryptographically bound to the object's path the same bucket/object as during sealing
|
||||||
// must be provided. On success the ObjectKey contains the decrypted sealed key.
|
// must be provided. On success the ObjectKey contains the decrypted sealed key.
|
||||||
func (key *ObjectKey) Unseal(sealedKey []byte, extKey, iv [32]byte, bucket, object string) error {
|
func (key *ObjectKey) Unseal(extKey [32]byte, sealedKey SealedKey, domain, bucket, object string) error {
|
||||||
var unsealedKey bytes.Buffer
|
var (
|
||||||
mac := hmac.New(sha256.New, extKey[:])
|
unsealConfig sio.Config
|
||||||
mac.Write(iv[:])
|
decryptedKey bytes.Buffer
|
||||||
mac.Write([]byte(filepath.Join(bucket, object)))
|
)
|
||||||
|
switch sealedKey.Algorithm {
|
||||||
|
default:
|
||||||
|
return Error{fmt.Sprintf("The sealing algorithm '%s' is not supported", sealedKey.Algorithm)}
|
||||||
|
case SealAlgorithm:
|
||||||
|
mac := hmac.New(sha256.New, extKey[:])
|
||||||
|
mac.Write(sealedKey.IV[:])
|
||||||
|
mac.Write([]byte(domain))
|
||||||
|
mac.Write([]byte(SealAlgorithm))
|
||||||
|
mac.Write([]byte(path.Join(bucket, object))) // use path.Join for canonical 'bucket/object'
|
||||||
|
unsealConfig = sio.Config{MinVersion: sio.Version20, Key: mac.Sum(nil)}
|
||||||
|
case InsecureSealAlgorithm:
|
||||||
|
sha := sha256.New()
|
||||||
|
sha.Write(extKey[:])
|
||||||
|
sha.Write(sealedKey.IV[:])
|
||||||
|
unsealConfig = sio.Config{MinVersion: sio.Version10, Key: sha.Sum(nil)}
|
||||||
|
}
|
||||||
|
|
||||||
if n, err := sio.Decrypt(&unsealedKey, bytes.NewReader(sealedKey), sio.Config{Key: mac.Sum(nil)}); n != 32 || err != nil {
|
if n, err := sio.Decrypt(&decryptedKey, bytes.NewReader(sealedKey.Key[:]), unsealConfig); n != 32 || err != nil {
|
||||||
return err // TODO(aead): upgrade sio to use sio.Error
|
return err // TODO(aead): upgrade sio to use sio.Error
|
||||||
}
|
}
|
||||||
copy(key[:], unsealedKey.Bytes())
|
copy(key[:], decryptedKey.Bytes())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,5 +138,5 @@ func (key ObjectKey) DerivePartKey(id uint32) (partKey [32]byte) {
|
|||||||
mac := hmac.New(sha256.New, key[:])
|
mac := hmac.New(sha256.New, key[:])
|
||||||
mac.Write(bin[:])
|
mac.Write(bin[:])
|
||||||
mac.Sum(partKey[:0])
|
mac.Sum(partKey[:0])
|
||||||
return
|
return partKey
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-21
@@ -20,6 +20,8 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var shortRandom = func(limit int64) io.Reader { return io.LimitReader(rand.Reader, limit) }
|
var shortRandom = func(limit int64) io.Reader { return io.LimitReader(rand.Reader, limit) }
|
||||||
@@ -37,14 +39,18 @@ var generateKeyTests = []struct {
|
|||||||
Random io.Reader
|
Random io.Reader
|
||||||
ShouldPass bool
|
ShouldPass bool
|
||||||
}{
|
}{
|
||||||
{ExtKey: [32]byte{}, Random: nil, ShouldPass: true}, // 0
|
{ExtKey: [32]byte{}, Random: nil, ShouldPass: true}, // 0
|
||||||
{ExtKey: [32]byte{}, Random: rand.Reader, ShouldPass: true}, // 1
|
{ExtKey: [32]byte{}, Random: rand.Reader, ShouldPass: true}, // 1
|
||||||
{ExtKey: [32]byte{}, Random: shortRandom(32), ShouldPass: true}, // 2
|
{ExtKey: [32]byte{}, Random: shortRandom(32), ShouldPass: true}, // 2
|
||||||
// {ExtKey: [32]byte{}, Random: shortRandom(31), ShouldPass: false}, // 3 See: https://github.com/minio/minio/issues/6064
|
{ExtKey: [32]byte{}, Random: shortRandom(31), ShouldPass: false}, // 3
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateKey(t *testing.T) {
|
func TestGenerateKey(t *testing.T) {
|
||||||
|
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
||||||
|
logger.Disable = true
|
||||||
|
|
||||||
for i, test := range generateKeyTests {
|
for i, test := range generateKeyTests {
|
||||||
|
i, test := i, test
|
||||||
func() {
|
func() {
|
||||||
defer recoverTest(i, test.ShouldPass, t)
|
defer recoverTest(i, test.ShouldPass, t)
|
||||||
key := GenerateKey(test.ExtKey, test.Random)
|
key := GenerateKey(test.ExtKey, test.Random)
|
||||||
@@ -55,38 +61,64 @@ func TestGenerateKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sealUnsealKeyTests = []struct {
|
var generateIVTests = []struct {
|
||||||
SealExtKey, SealIV [32]byte
|
Random io.Reader
|
||||||
SealBucket, SealObject string
|
ShouldPass bool
|
||||||
|
}{
|
||||||
|
{Random: nil, ShouldPass: true}, // 0
|
||||||
|
{Random: rand.Reader, ShouldPass: true}, // 1
|
||||||
|
{Random: shortRandom(32), ShouldPass: true}, // 2
|
||||||
|
{Random: shortRandom(31), ShouldPass: false}, // 3
|
||||||
|
}
|
||||||
|
|
||||||
UnsealExtKey, UnsealIV [32]byte
|
func TestGenerateIV(t *testing.T) {
|
||||||
UnsealBucket, UnsealObject string
|
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
||||||
|
logger.Disable = true
|
||||||
|
|
||||||
|
for i, test := range generateIVTests {
|
||||||
|
i, test := i, test
|
||||||
|
func() {
|
||||||
|
defer recoverTest(i, test.ShouldPass, t)
|
||||||
|
iv := GenerateIV(test.Random)
|
||||||
|
if iv == [32]byte{} {
|
||||||
|
t.Errorf("Test %d: generated IV is zero IV", i) // check that we generate random and unique IV
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sealUnsealKeyTests = []struct {
|
||||||
|
SealExtKey, SealIV [32]byte
|
||||||
|
SealDomain, SealBucket, SealObject string
|
||||||
|
|
||||||
|
UnsealExtKey [32]byte
|
||||||
|
UnsealDomain, UnsealBucket, UnsealObject string
|
||||||
|
|
||||||
ShouldPass bool
|
ShouldPass bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object",
|
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
|
||||||
UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{}, UnsealBucket: "bucket", UnsealObject: "object",
|
UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object",
|
||||||
ShouldPass: true,
|
ShouldPass: true,
|
||||||
}, // 0
|
}, // 0
|
||||||
{
|
{
|
||||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object",
|
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
|
||||||
UnsealExtKey: [32]byte{1}, UnsealIV: [32]byte{0}, UnsealBucket: "bucket", UnsealObject: "object",
|
UnsealExtKey: [32]byte{1}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object", // different ext-key
|
||||||
ShouldPass: false,
|
ShouldPass: false,
|
||||||
}, // 1
|
}, // 1
|
||||||
{
|
{
|
||||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object",
|
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-S3", SealBucket: "bucket", SealObject: "object",
|
||||||
UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{1}, UnsealBucket: "bucket", UnsealObject: "object",
|
UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object", // different domain
|
||||||
ShouldPass: false,
|
ShouldPass: false,
|
||||||
}, // 2
|
}, // 2
|
||||||
{
|
{
|
||||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object",
|
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
|
||||||
UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{}, UnsealBucket: "Bucket", UnsealObject: "object",
|
UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "Bucket", UnsealObject: "object", // different bucket
|
||||||
ShouldPass: false,
|
ShouldPass: false,
|
||||||
}, // 3
|
}, // 3
|
||||||
{
|
{
|
||||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealBucket: "bucket", SealObject: "object",
|
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
|
||||||
UnsealExtKey: [32]byte{}, UnsealIV: [32]byte{}, UnsealBucket: "bucket", UnsealObject: "Object",
|
UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "Object", // different object
|
||||||
ShouldPass: false,
|
ShouldPass: false,
|
||||||
}, // 4
|
}, // 4
|
||||||
}
|
}
|
||||||
@@ -94,13 +126,22 @@ var sealUnsealKeyTests = []struct {
|
|||||||
func TestSealUnsealKey(t *testing.T) {
|
func TestSealUnsealKey(t *testing.T) {
|
||||||
for i, test := range sealUnsealKeyTests {
|
for i, test := range sealUnsealKeyTests {
|
||||||
key := GenerateKey(test.SealExtKey, rand.Reader)
|
key := GenerateKey(test.SealExtKey, rand.Reader)
|
||||||
sealedKey := key.Seal(test.SealExtKey, test.SealIV, test.SealBucket, test.SealObject)
|
sealedKey := key.Seal(test.SealExtKey, test.SealIV, test.SealDomain, test.SealBucket, test.SealObject)
|
||||||
if err := key.Unseal(sealedKey, test.UnsealExtKey, test.UnsealIV, test.UnsealBucket, test.UnsealObject); err == nil && !test.ShouldPass {
|
if err := key.Unseal(test.UnsealExtKey, sealedKey, test.UnsealDomain, test.UnsealBucket, test.UnsealObject); err == nil && !test.ShouldPass {
|
||||||
t.Errorf("Test %d should fail but passed successfully", i)
|
t.Errorf("Test %d should fail but passed successfully", i)
|
||||||
} else if err != nil && test.ShouldPass {
|
} else if err != nil && test.ShouldPass {
|
||||||
t.Errorf("Test %d should pass put failed: %v", i, err)
|
t.Errorf("Test %d should pass put failed: %v", i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test legacy InsecureSealAlgorithm
|
||||||
|
var extKey, iv [32]byte
|
||||||
|
key := GenerateKey(extKey, rand.Reader)
|
||||||
|
sealedKey := key.Seal(extKey, iv, "SSE-S3", "bucket", "object")
|
||||||
|
sealedKey.Algorithm = InsecureSealAlgorithm
|
||||||
|
if err := key.Unseal(extKey, sealedKey, "SSE-S3", "bucket", "object"); err == nil {
|
||||||
|
t.Errorf("'%s' test succeeded but it should fail because the legacy algorithm was used", sealedKey.Algorithm)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var derivePartKeyTest = []struct {
|
var derivePartKeyTest = []struct {
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
sha256 "github.com/minio/sha256-simd"
|
||||||
|
"github.com/minio/sio"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context is a list of key-value pairs cryptographically
|
||||||
|
// associated with a certain object.
|
||||||
|
type Context map[string]string
|
||||||
|
|
||||||
|
// WriteTo writes the context in a canonical from to w.
|
||||||
|
// It returns the number of bytes and the first error
|
||||||
|
// encounter during writing to w, if any.
|
||||||
|
//
|
||||||
|
// WriteTo sorts the context keys and writes the sorted
|
||||||
|
// key-value pairs as canonical JSON object to w.
|
||||||
|
func (c Context) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
sortedKeys := make(sort.StringSlice, 0, len(c))
|
||||||
|
for k := range c {
|
||||||
|
sortedKeys = append(sortedKeys, k)
|
||||||
|
}
|
||||||
|
sort.Sort(sortedKeys)
|
||||||
|
|
||||||
|
nn, err := io.WriteString(w, "{")
|
||||||
|
if err != nil {
|
||||||
|
return n + int64(nn), err
|
||||||
|
}
|
||||||
|
n += int64(nn)
|
||||||
|
for i, k := range sortedKeys {
|
||||||
|
s := fmt.Sprintf("\"%s\":\"%s\",", k, c[k])
|
||||||
|
if i == len(sortedKeys)-1 {
|
||||||
|
s = s[:len(s)-1] // remove last ','
|
||||||
|
}
|
||||||
|
|
||||||
|
nn, err = io.WriteString(w, s)
|
||||||
|
if err != nil {
|
||||||
|
return n + int64(nn), err
|
||||||
|
}
|
||||||
|
n += int64(nn)
|
||||||
|
}
|
||||||
|
nn, err = io.WriteString(w, "}")
|
||||||
|
return n + int64(nn), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// KMS represents an active and authenticted connection
|
||||||
|
// to a Key-Management-Service. It supports generating
|
||||||
|
// data key generation and unsealing of KMS-generated
|
||||||
|
// data keys.
|
||||||
|
type KMS interface {
|
||||||
|
// GenerateKey generates a new random data key using
|
||||||
|
// the master key referenced by the keyID. It returns
|
||||||
|
// the plaintext key and the sealed plaintext key
|
||||||
|
// on success.
|
||||||
|
//
|
||||||
|
// The context is cryptographically bound to the
|
||||||
|
// generated key. The same context must be provided
|
||||||
|
// again to unseal the generated key.
|
||||||
|
GenerateKey(keyID string, context Context) (key [32]byte, sealedKey []byte, err error)
|
||||||
|
|
||||||
|
// UnsealKey unseals the sealedKey using the master key
|
||||||
|
// referenced by the keyID. The provided context must
|
||||||
|
// match the context used to generate the sealed key.
|
||||||
|
UnsealKey(keyID string, sealedKey []byte, context Context) (key [32]byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type masterKeyKMS struct {
|
||||||
|
masterKey [32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKMS returns a basic KMS implementation from a single 256 bit master key.
|
||||||
|
//
|
||||||
|
// The KMS accepts any keyID but binds the keyID and context cryptographically
|
||||||
|
// to the generated keys.
|
||||||
|
func NewKMS(key [32]byte) KMS { return &masterKeyKMS{masterKey: key} }
|
||||||
|
|
||||||
|
func (kms *masterKeyKMS) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
|
||||||
|
if _, err = io.ReadFull(rand.Reader, key[:]); err != nil {
|
||||||
|
logger.CriticalIf(context.Background(), errOutOfEntropy)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
buffer bytes.Buffer
|
||||||
|
derivedKey = kms.deriveKey(keyID, ctx)
|
||||||
|
)
|
||||||
|
if n, err := sio.Encrypt(&buffer, bytes.NewReader(key[:]), sio.Config{Key: derivedKey[:]}); err != nil || n != 64 {
|
||||||
|
logger.CriticalIf(context.Background(), errors.New("KMS: unable to encrypt data key"))
|
||||||
|
}
|
||||||
|
sealedKey = buffer.Bytes()
|
||||||
|
return key, sealedKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kms *masterKeyKMS) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
|
||||||
|
var (
|
||||||
|
buffer bytes.Buffer
|
||||||
|
derivedKey = kms.deriveKey(keyID, ctx)
|
||||||
|
)
|
||||||
|
if n, err := sio.Decrypt(&buffer, bytes.NewReader(sealedKey), sio.Config{Key: derivedKey[:]}); err != nil || n != 32 {
|
||||||
|
return key, err // TODO(aead): upgrade sio to use sio.Error
|
||||||
|
}
|
||||||
|
copy(key[:], buffer.Bytes())
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kms *masterKeyKMS) deriveKey(keyID string, context Context) (key [32]byte) {
|
||||||
|
if context == nil {
|
||||||
|
context = Context{}
|
||||||
|
}
|
||||||
|
mac := hmac.New(sha256.New, kms.masterKey[:])
|
||||||
|
mac.Write([]byte(keyID))
|
||||||
|
context.WriteTo(mac)
|
||||||
|
mac.Sum(key[:0])
|
||||||
|
return key
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var masterKeyKMSTests = []struct {
|
||||||
|
GenKeyID, UnsealKeyID string
|
||||||
|
GenContext, UnsealContext Context
|
||||||
|
|
||||||
|
ShouldFail bool
|
||||||
|
}{
|
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{}, UnsealContext: nil, ShouldFail: false}, // 0
|
||||||
|
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7f", GenContext: Context{}, UnsealContext: Context{}, ShouldFail: false}, // 1
|
||||||
|
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7f", GenContext: Context{"bucket": "object"}, UnsealContext: Context{"bucket": "object"}, ShouldFail: false}, // 2
|
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"bucket": path.Join("bucket", "object")}, UnsealContext: Context{"bucket": path.Join("bucket", "object")}, ShouldFail: false}, // 3
|
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"a": "a", "0": "0", "b": "b"}, UnsealContext: Context{"b": "b", "a": "a", "0": "0"}, ShouldFail: false}, // 4
|
||||||
|
|
||||||
|
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7e", GenContext: Context{}, UnsealContext: Context{}, ShouldFail: true}, // 5
|
||||||
|
{GenKeyID: "ac47be7f", UnsealKeyID: "ac47be7f", GenContext: Context{"bucket": "object"}, UnsealContext: Context{"Bucket": "object"}, ShouldFail: true}, // 6
|
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"bucket": path.Join("bucket", "Object")}, UnsealContext: Context{"bucket": path.Join("bucket", "object")}, ShouldFail: true}, // 7
|
||||||
|
{GenKeyID: "", UnsealKeyID: "", GenContext: Context{"a": "a", "0": "1", "b": "b"}, UnsealContext: Context{"b": "b", "a": "a", "0": "0"}, ShouldFail: true}, // 8
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMasterKeyKMS(t *testing.T) {
|
||||||
|
kms := NewKMS([32]byte{})
|
||||||
|
for i, test := range masterKeyKMSTests {
|
||||||
|
key, sealedKey, err := kms.GenerateKey(test.GenKeyID, test.GenContext)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: KMS failed to generate key: %v", i, err)
|
||||||
|
}
|
||||||
|
unsealedKey, err := kms.UnsealKey(test.UnsealKeyID, sealedKey, test.UnsealContext)
|
||||||
|
if err != nil && !test.ShouldFail {
|
||||||
|
t.Errorf("Test %d: KMS failed to unseal the generated key: %v", i, err)
|
||||||
|
}
|
||||||
|
if err == nil && test.ShouldFail {
|
||||||
|
t.Errorf("Test %d: KMS unsealed the generated successfully but should have failed", i)
|
||||||
|
}
|
||||||
|
if !test.ShouldFail && !bytes.Equal(key[:], unsealedKey[:]) {
|
||||||
|
t.Errorf("Test %d: The generated and unsealed key differ", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextWriteToTests = []struct {
|
||||||
|
Context Context
|
||||||
|
ExpectedJSON string
|
||||||
|
}{
|
||||||
|
{Context: Context{}, ExpectedJSON: "{}"}, // 0
|
||||||
|
{Context: Context{"a": "b"}, ExpectedJSON: `{"a":"b"}`}, // 1
|
||||||
|
{Context: Context{"a": "b", "c": "d"}, ExpectedJSON: `{"a":"b","c":"d"}`}, // 2
|
||||||
|
{Context: Context{"c": "d", "a": "b"}, ExpectedJSON: `{"a":"b","c":"d"}`}, // 3
|
||||||
|
{Context: Context{"0": "1", "-": "2", ".": "#"}, ExpectedJSON: `{"-":"2",".":"#","0":"1"}`}, // 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextWriteTo(t *testing.T) {
|
||||||
|
for i, test := range contextWriteToTests {
|
||||||
|
var jsonContext strings.Builder
|
||||||
|
if _, err := test.Context.WriteTo(&jsonContext); err != nil {
|
||||||
|
t.Errorf("Test %d: Failed to encode context: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s := jsonContext.String(); s != test.ExpectedJSON {
|
||||||
|
t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsMultiPart returns true if the object metadata indicates
|
||||||
|
// that it was uploaded using some form of server-side-encryption
|
||||||
|
// and the S3 multipart API.
|
||||||
|
func IsMultiPart(metadata map[string]string) bool {
|
||||||
|
if _, ok := metadata[SSEMultipart]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEncrypted returns true if the object metadata indicates
|
||||||
|
// that it was uploaded using some form of server-side-encryption.
|
||||||
|
//
|
||||||
|
// IsEncrypted only checks whether the metadata contains at least
|
||||||
|
// one entry indicating SSE-C or SSE-S3.
|
||||||
|
func IsEncrypted(metadata map[string]string) bool {
|
||||||
|
if _, ok := metadata[SSEIV]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := metadata[SSESealAlgorithm]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if IsMultiPart(metadata) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if S3.IsEncrypted(metadata) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if SSEC.IsEncrypted(metadata) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEncrypted returns true if the object metadata indicates
|
||||||
|
// that the object was uploaded using SSE-S3.
|
||||||
|
func (s3) IsEncrypted(metadata map[string]string) bool {
|
||||||
|
if _, ok := metadata[S3SealedKey]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := metadata[S3KMSKeyID]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := metadata[S3KMSSealedKey]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEncrypted returns true if the object metadata indicates
|
||||||
|
// that the object was uploaded using SSE-C.
|
||||||
|
func (ssec) IsEncrypted(metadata map[string]string) bool {
|
||||||
|
if _, ok := metadata[SSECSealedKey]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMultipartMetadata adds the multipart flag entry to metadata
|
||||||
|
// and returns modifed metadata. It allocates a new metadata map if
|
||||||
|
// metadata is nil.
|
||||||
|
func CreateMultipartMetadata(metadata map[string]string) map[string]string {
|
||||||
|
if metadata == nil {
|
||||||
|
metadata = map[string]string{}
|
||||||
|
}
|
||||||
|
metadata[SSEMultipart] = ""
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMetadata encodes the keyID, the sealed kms data key and the sealed key
|
||||||
|
// into the metadata and returns the modified metadata. It allocates a new
|
||||||
|
// metadata map if metadata is nil.
|
||||||
|
func (s3) CreateMetadata(metadata map[string]string, keyID string, kmsKey []byte, sealedKey SealedKey) map[string]string {
|
||||||
|
if sealedKey.Algorithm != SealAlgorithm {
|
||||||
|
logger.CriticalIf(context.Background(), fmt.Errorf("The seal algorithm '%s' is invalid for SSE-S3", sealedKey.Algorithm))
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata == nil {
|
||||||
|
metadata = map[string]string{}
|
||||||
|
}
|
||||||
|
metadata[S3KMSKeyID] = keyID
|
||||||
|
metadata[SSESealAlgorithm] = sealedKey.Algorithm
|
||||||
|
metadata[SSEIV] = base64.StdEncoding.EncodeToString(sealedKey.IV[:])
|
||||||
|
metadata[S3SealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Key[:])
|
||||||
|
metadata[S3KMSSealedKey] = base64.StdEncoding.EncodeToString(kmsKey)
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMetadata extracts all SSE-S3 related values from the object metadata
|
||||||
|
// and checks whether they are well-formed. It returns the KMS key-ID, the
|
||||||
|
// sealed KMS key and the sealed object key on success.
|
||||||
|
func (s3) ParseMetadata(metadata map[string]string) (keyID string, kmsKey []byte, sealedKey SealedKey, err error) {
|
||||||
|
// Extract all required values from object metadata
|
||||||
|
b64IV, ok := metadata[SSEIV]
|
||||||
|
if !ok {
|
||||||
|
return keyID, kmsKey, sealedKey, errMissingInternalIV
|
||||||
|
}
|
||||||
|
algorithm, ok := metadata[SSESealAlgorithm]
|
||||||
|
if !ok {
|
||||||
|
return keyID, kmsKey, sealedKey, errMissingInternalSealAlgorithm
|
||||||
|
}
|
||||||
|
b64SealedKey, ok := metadata[S3SealedKey]
|
||||||
|
if !ok {
|
||||||
|
return keyID, kmsKey, sealedKey, Error{"The object metadata is missing the internal sealed key for SSE-S3"}
|
||||||
|
}
|
||||||
|
keyID, ok = metadata[S3KMSKeyID]
|
||||||
|
if !ok {
|
||||||
|
return keyID, kmsKey, sealedKey, Error{"The object metadata is missing the internal KMS key-ID for SSE-S3"}
|
||||||
|
}
|
||||||
|
b64KMSSealedKey, ok := metadata[S3KMSSealedKey]
|
||||||
|
if !ok {
|
||||||
|
return keyID, kmsKey, sealedKey, Error{"The object metadata is missing the internal sealed KMS data key for SSE-S3"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether all extracted values are well-formed
|
||||||
|
iv, err := base64.StdEncoding.DecodeString(b64IV)
|
||||||
|
if err != nil || len(iv) != 32 {
|
||||||
|
return keyID, kmsKey, sealedKey, errInvalidInternalIV
|
||||||
|
}
|
||||||
|
if algorithm != SealAlgorithm {
|
||||||
|
return keyID, kmsKey, sealedKey, errInvalidInternalSealAlgorithm
|
||||||
|
}
|
||||||
|
encryptedKey, err := base64.StdEncoding.DecodeString(b64SealedKey)
|
||||||
|
if err != nil || len(encryptedKey) != 64 {
|
||||||
|
return keyID, kmsKey, sealedKey, Error{"The internal sealed key for SSE-S3 is invalid"}
|
||||||
|
}
|
||||||
|
kmsKey, err = base64.StdEncoding.DecodeString(b64KMSSealedKey)
|
||||||
|
if err != nil {
|
||||||
|
return keyID, kmsKey, sealedKey, Error{"The internal sealed KMS data key for SSE-S3 is invalid"}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealedKey.Algorithm = algorithm
|
||||||
|
copy(sealedKey.IV[:], iv)
|
||||||
|
copy(sealedKey.Key[:], encryptedKey)
|
||||||
|
return keyID, kmsKey, sealedKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMetadata encodes the sealed key into the metadata and returns the modified metadata.
|
||||||
|
// It allocates a new metadata map if metadata is nil.
|
||||||
|
func (ssec) CreateMetadata(metadata map[string]string, sealedKey SealedKey) map[string]string {
|
||||||
|
if sealedKey.Algorithm != SealAlgorithm {
|
||||||
|
logger.CriticalIf(context.Background(), fmt.Errorf("The seal algorithm '%s' is invalid for SSE-C", sealedKey.Algorithm))
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata == nil {
|
||||||
|
metadata = map[string]string{}
|
||||||
|
}
|
||||||
|
metadata[SSESealAlgorithm] = SealAlgorithm
|
||||||
|
metadata[SSEIV] = base64.StdEncoding.EncodeToString(sealedKey.IV[:])
|
||||||
|
metadata[SSECSealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Key[:])
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMetadata extracts all SSE-C related values from the object metadata
|
||||||
|
// and checks whether they are well-formed. It returns the sealed object key
|
||||||
|
// on success.
|
||||||
|
func (ssec) ParseMetadata(metadata map[string]string) (sealedKey SealedKey, err error) {
|
||||||
|
// Extract all required values from object metadata
|
||||||
|
b64IV, ok := metadata[SSEIV]
|
||||||
|
if !ok {
|
||||||
|
return sealedKey, errMissingInternalIV
|
||||||
|
}
|
||||||
|
algorithm, ok := metadata[SSESealAlgorithm]
|
||||||
|
if !ok {
|
||||||
|
return sealedKey, errMissingInternalSealAlgorithm
|
||||||
|
}
|
||||||
|
b64SealedKey, ok := metadata[SSECSealedKey]
|
||||||
|
if !ok {
|
||||||
|
return sealedKey, Error{"The object metadata is missing the internal sealed key for SSE-C"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether all extracted values are well-formed
|
||||||
|
iv, err := base64.StdEncoding.DecodeString(b64IV)
|
||||||
|
if err != nil || len(iv) != 32 {
|
||||||
|
return sealedKey, errInvalidInternalIV
|
||||||
|
}
|
||||||
|
if algorithm != SealAlgorithm && algorithm != InsecureSealAlgorithm {
|
||||||
|
return sealedKey, errInvalidInternalSealAlgorithm
|
||||||
|
}
|
||||||
|
encryptedKey, err := base64.StdEncoding.DecodeString(b64SealedKey)
|
||||||
|
if err != nil || len(encryptedKey) != 64 {
|
||||||
|
return sealedKey, Error{"The internal sealed key for SSE-C is invalid"}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealedKey.Algorithm = algorithm
|
||||||
|
copy(sealedKey.IV[:], iv)
|
||||||
|
copy(sealedKey.Key[:], encryptedKey)
|
||||||
|
return sealedKey, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var isMultipartTests = []struct {
|
||||||
|
Metadata map[string]string
|
||||||
|
Multipart bool
|
||||||
|
}{
|
||||||
|
{Multipart: true, Metadata: map[string]string{SSEMultipart: ""}}, // 0
|
||||||
|
{Multipart: true, Metadata: map[string]string{"X-Minio-Internal-Encrypted-Multipart": ""}}, // 1
|
||||||
|
{Multipart: true, Metadata: map[string]string{SSEMultipart: "some-value"}}, // 2
|
||||||
|
{Multipart: false, Metadata: map[string]string{"": ""}}, // 3
|
||||||
|
{Multipart: false, Metadata: map[string]string{"X-Minio-Internal-EncryptedMultipart": ""}}, // 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsMultipart(t *testing.T) {
|
||||||
|
for i, test := range isMultipartTests {
|
||||||
|
if isMultipart := IsMultiPart(test.Metadata); isMultipart != test.Multipart {
|
||||||
|
t.Errorf("Test %d: got '%v' - want '%v'", i, isMultipart, test.Multipart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEncryptedTests = []struct {
|
||||||
|
Metadata map[string]string
|
||||||
|
Encrypted bool
|
||||||
|
}{
|
||||||
|
{Encrypted: true, Metadata: map[string]string{SSEMultipart: ""}}, // 0
|
||||||
|
{Encrypted: true, Metadata: map[string]string{SSEIV: ""}}, // 1
|
||||||
|
{Encrypted: true, Metadata: map[string]string{SSESealAlgorithm: ""}}, // 2
|
||||||
|
{Encrypted: true, Metadata: map[string]string{SSECSealedKey: ""}}, // 3
|
||||||
|
{Encrypted: true, Metadata: map[string]string{S3SealedKey: ""}}, // 4
|
||||||
|
{Encrypted: true, Metadata: map[string]string{S3KMSKeyID: ""}}, // 5
|
||||||
|
{Encrypted: true, Metadata: map[string]string{S3KMSSealedKey: ""}}, // 6
|
||||||
|
{Encrypted: false, Metadata: map[string]string{"": ""}}, // 7
|
||||||
|
{Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsEncrypted(t *testing.T) {
|
||||||
|
for i, test := range isEncryptedTests {
|
||||||
|
if isEncrypted := IsEncrypted(test.Metadata); isEncrypted != test.Encrypted {
|
||||||
|
t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var s3IsEncryptedTests = []struct {
|
||||||
|
Metadata map[string]string
|
||||||
|
Encrypted bool
|
||||||
|
}{
|
||||||
|
{Encrypted: false, Metadata: map[string]string{SSEMultipart: ""}}, // 0
|
||||||
|
{Encrypted: false, Metadata: map[string]string{SSEIV: ""}}, // 1
|
||||||
|
{Encrypted: false, Metadata: map[string]string{SSESealAlgorithm: ""}}, // 2
|
||||||
|
{Encrypted: false, Metadata: map[string]string{SSECSealedKey: ""}}, // 3
|
||||||
|
{Encrypted: true, Metadata: map[string]string{S3SealedKey: ""}}, // 4
|
||||||
|
{Encrypted: true, Metadata: map[string]string{S3KMSKeyID: ""}}, // 5
|
||||||
|
{Encrypted: true, Metadata: map[string]string{S3KMSSealedKey: ""}}, // 6
|
||||||
|
{Encrypted: false, Metadata: map[string]string{"": ""}}, // 7
|
||||||
|
{Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestS3IsEncrypted(t *testing.T) {
|
||||||
|
for i, test := range s3IsEncryptedTests {
|
||||||
|
if isEncrypted := S3.IsEncrypted(test.Metadata); isEncrypted != test.Encrypted {
|
||||||
|
t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssecIsEncryptedTests = []struct {
|
||||||
|
Metadata map[string]string
|
||||||
|
Encrypted bool
|
||||||
|
}{
|
||||||
|
{Encrypted: false, Metadata: map[string]string{SSEMultipart: ""}}, // 0
|
||||||
|
{Encrypted: false, Metadata: map[string]string{SSEIV: ""}}, // 1
|
||||||
|
{Encrypted: false, Metadata: map[string]string{SSESealAlgorithm: ""}}, // 2
|
||||||
|
{Encrypted: true, Metadata: map[string]string{SSECSealedKey: ""}}, // 3
|
||||||
|
{Encrypted: false, Metadata: map[string]string{S3SealedKey: ""}}, // 4
|
||||||
|
{Encrypted: false, Metadata: map[string]string{S3KMSKeyID: ""}}, // 5
|
||||||
|
{Encrypted: false, Metadata: map[string]string{S3KMSSealedKey: ""}}, // 6
|
||||||
|
{Encrypted: false, Metadata: map[string]string{"": ""}}, // 7
|
||||||
|
{Encrypted: false, Metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption": ""}}, // 8
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECIsEncrypted(t *testing.T) {
|
||||||
|
for i, test := range ssecIsEncryptedTests {
|
||||||
|
if isEncrypted := SSEC.IsEncrypted(test.Metadata); isEncrypted != test.Encrypted {
|
||||||
|
t.Errorf("Test %d: got '%v' - want '%v'", i, isEncrypted, test.Encrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var s3ParseMetadataTests = []struct {
|
||||||
|
Metadata map[string]string
|
||||||
|
ExpectedErr error
|
||||||
|
|
||||||
|
DataKey []byte
|
||||||
|
KeyID string
|
||||||
|
SealedKey SealedKey
|
||||||
|
}{
|
||||||
|
{ExpectedErr: errMissingInternalIV, Metadata: map[string]string{}, DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{}}, // 0
|
||||||
|
{
|
||||||
|
ExpectedErr: errMissingInternalSealAlgorithm, Metadata: map[string]string{SSEIV: ""},
|
||||||
|
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
|
||||||
|
}, // 1
|
||||||
|
{
|
||||||
|
ExpectedErr: Error{"The object metadata is missing the internal sealed key for SSE-S3"},
|
||||||
|
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: ""}, DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
|
||||||
|
}, // 2
|
||||||
|
{
|
||||||
|
ExpectedErr: Error{"The object metadata is missing the internal KMS key-ID for SSE-S3"},
|
||||||
|
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", S3SealedKey: ""}, DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
|
||||||
|
}, // 3
|
||||||
|
{
|
||||||
|
ExpectedErr: Error{"The object metadata is missing the internal sealed KMS data key for SSE-S3"},
|
||||||
|
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", S3SealedKey: "", S3KMSKeyID: ""},
|
||||||
|
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
|
||||||
|
}, // 4
|
||||||
|
{
|
||||||
|
ExpectedErr: errInvalidInternalIV,
|
||||||
|
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", S3SealedKey: "", S3KMSKeyID: "", S3KMSSealedKey: ""},
|
||||||
|
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
|
||||||
|
}, // 5
|
||||||
|
{
|
||||||
|
ExpectedErr: errInvalidInternalSealAlgorithm,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: "", S3SealedKey: "", S3KMSKeyID: "", S3KMSSealedKey: "",
|
||||||
|
},
|
||||||
|
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
|
||||||
|
}, // 6
|
||||||
|
{
|
||||||
|
ExpectedErr: Error{"The internal sealed key for SSE-S3 is invalid"},
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm, S3SealedKey: "",
|
||||||
|
S3KMSKeyID: "", S3KMSSealedKey: "",
|
||||||
|
},
|
||||||
|
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{},
|
||||||
|
}, // 7
|
||||||
|
{
|
||||||
|
ExpectedErr: Error{"The internal sealed KMS data key for SSE-S3 is invalid"},
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm,
|
||||||
|
S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)), S3KMSKeyID: "key-1",
|
||||||
|
S3KMSSealedKey: ".MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=", // invalid base64
|
||||||
|
},
|
||||||
|
DataKey: []byte{}, KeyID: "key-1", SealedKey: SealedKey{},
|
||||||
|
}, // 8
|
||||||
|
{
|
||||||
|
ExpectedErr: nil,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm,
|
||||||
|
S3SealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)), S3KMSKeyID: "", S3KMSSealedKey: "",
|
||||||
|
},
|
||||||
|
DataKey: []byte{}, KeyID: "", SealedKey: SealedKey{Algorithm: SealAlgorithm},
|
||||||
|
}, // 9
|
||||||
|
{
|
||||||
|
ExpectedErr: nil,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEIV: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 31)...)), SSESealAlgorithm: SealAlgorithm,
|
||||||
|
S3SealedKey: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 63)...)), S3KMSKeyID: "key-1",
|
||||||
|
S3KMSSealedKey: base64.StdEncoding.EncodeToString(make([]byte, 48)),
|
||||||
|
},
|
||||||
|
DataKey: make([]byte, 48), KeyID: "key-1", SealedKey: SealedKey{Algorithm: SealAlgorithm, Key: [64]byte{1}, IV: [32]byte{1}},
|
||||||
|
}, // 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestS3ParseMetadata(t *testing.T) {
|
||||||
|
for i, test := range s3ParseMetadataTests {
|
||||||
|
keyID, dataKey, sealedKey, err := S3.ParseMetadata(test.Metadata)
|
||||||
|
if err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: got error '%v' - want error '%v'", i, err, test.ExpectedErr)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(dataKey, test.DataKey) {
|
||||||
|
t.Errorf("Test %d: got data key '%v' - want data key '%v'", i, dataKey, test.DataKey)
|
||||||
|
}
|
||||||
|
if keyID != test.KeyID {
|
||||||
|
t.Errorf("Test %d: got key-ID '%v' - want key-ID '%v'", i, keyID, test.KeyID)
|
||||||
|
}
|
||||||
|
if sealedKey.Algorithm != test.SealedKey.Algorithm {
|
||||||
|
t.Errorf("Test %d: got sealed key algorithm '%v' - want sealed key algorithm '%v'", i, sealedKey.Algorithm, test.SealedKey.Algorithm)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) {
|
||||||
|
t.Errorf("Test %d: got sealed key '%v' - want sealed key '%v'", i, sealedKey.Key, test.SealedKey.Key)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) {
|
||||||
|
t.Errorf("Test %d: got sealed key IV '%v' - want sealed key IV '%v'", i, sealedKey.IV, test.SealedKey.IV)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssecParseMetadataTests = []struct {
|
||||||
|
Metadata map[string]string
|
||||||
|
ExpectedErr error
|
||||||
|
|
||||||
|
SealedKey SealedKey
|
||||||
|
}{
|
||||||
|
{ExpectedErr: errMissingInternalIV, Metadata: map[string]string{}, SealedKey: SealedKey{}}, // 0
|
||||||
|
{ExpectedErr: errMissingInternalSealAlgorithm, Metadata: map[string]string{SSEIV: ""}, SealedKey: SealedKey{}}, // 1
|
||||||
|
{
|
||||||
|
ExpectedErr: Error{"The object metadata is missing the internal sealed key for SSE-C"},
|
||||||
|
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: ""}, SealedKey: SealedKey{},
|
||||||
|
}, // 2
|
||||||
|
{
|
||||||
|
ExpectedErr: errInvalidInternalIV,
|
||||||
|
Metadata: map[string]string{SSEIV: "", SSESealAlgorithm: "", SSECSealedKey: ""}, SealedKey: SealedKey{},
|
||||||
|
}, // 3
|
||||||
|
{
|
||||||
|
ExpectedErr: errInvalidInternalSealAlgorithm,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: "", SSECSealedKey: "",
|
||||||
|
},
|
||||||
|
SealedKey: SealedKey{},
|
||||||
|
}, // 4
|
||||||
|
{
|
||||||
|
ExpectedErr: Error{"The internal sealed key for SSE-C is invalid"},
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm, SSECSealedKey: "",
|
||||||
|
},
|
||||||
|
SealedKey: SealedKey{},
|
||||||
|
}, // 5
|
||||||
|
{
|
||||||
|
ExpectedErr: nil,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEIV: base64.StdEncoding.EncodeToString(make([]byte, 32)), SSESealAlgorithm: SealAlgorithm,
|
||||||
|
SSECSealedKey: base64.StdEncoding.EncodeToString(make([]byte, 64)),
|
||||||
|
},
|
||||||
|
SealedKey: SealedKey{Algorithm: SealAlgorithm},
|
||||||
|
}, // 6
|
||||||
|
{
|
||||||
|
ExpectedErr: nil,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
SSEIV: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 31)...)), SSESealAlgorithm: InsecureSealAlgorithm,
|
||||||
|
SSECSealedKey: base64.StdEncoding.EncodeToString(append([]byte{1}, make([]byte, 63)...)),
|
||||||
|
},
|
||||||
|
SealedKey: SealedKey{Algorithm: InsecureSealAlgorithm, Key: [64]byte{1}, IV: [32]byte{1}},
|
||||||
|
}, // 7
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateMultipartMetadata(t *testing.T) {
|
||||||
|
metadata := CreateMultipartMetadata(nil)
|
||||||
|
if v, ok := metadata[SSEMultipart]; !ok || v != "" {
|
||||||
|
t.Errorf("Metadata is missing the correct value for '%s': got '%s' - want '%s'", SSEMultipart, v, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECParseMetadata(t *testing.T) {
|
||||||
|
for i, test := range ssecParseMetadataTests {
|
||||||
|
sealedKey, err := SSEC.ParseMetadata(test.Metadata)
|
||||||
|
if err != test.ExpectedErr {
|
||||||
|
t.Errorf("Test %d: got error '%v' - want error '%v'", i, err, test.ExpectedErr)
|
||||||
|
}
|
||||||
|
if sealedKey.Algorithm != test.SealedKey.Algorithm {
|
||||||
|
t.Errorf("Test %d: got sealed key algorithm '%v' - want sealed key algorithm '%v'", i, sealedKey.Algorithm, test.SealedKey.Algorithm)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) {
|
||||||
|
t.Errorf("Test %d: got sealed key '%v' - want sealed key '%v'", i, sealedKey.Key, test.SealedKey.Key)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) {
|
||||||
|
t.Errorf("Test %d: got sealed key IV '%v' - want sealed key IV '%v'", i, sealedKey.IV, test.SealedKey.IV)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var s3CreateMetadataTests = []struct {
|
||||||
|
KeyID string
|
||||||
|
SealedDataKey []byte
|
||||||
|
SealedKey SealedKey
|
||||||
|
}{
|
||||||
|
{KeyID: "", SealedDataKey: make([]byte, 48), SealedKey: SealedKey{Algorithm: SealAlgorithm}},
|
||||||
|
{KeyID: "cafebabe", SealedDataKey: make([]byte, 48), SealedKey: SealedKey{Algorithm: SealAlgorithm}},
|
||||||
|
{KeyID: "deadbeef", SealedDataKey: make([]byte, 32), SealedKey: SealedKey{IV: [32]byte{0xf7}, Key: [64]byte{0xea}, Algorithm: SealAlgorithm}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestS3CreateMetadata(t *testing.T) {
|
||||||
|
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
||||||
|
logger.Disable = true
|
||||||
|
for i, test := range s3CreateMetadataTests {
|
||||||
|
metadata := S3.CreateMetadata(nil, test.KeyID, test.SealedDataKey, test.SealedKey)
|
||||||
|
keyID, kmsKey, sealedKey, err := S3.ParseMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: failed to parse metadata: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if keyID != test.KeyID {
|
||||||
|
t.Errorf("Test %d: Key-ID mismatch: got '%s' - want '%s'", i, keyID, test.KeyID)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(kmsKey, test.SealedDataKey) {
|
||||||
|
t.Errorf("Test %d: sealed KMS data mismatch: got '%v' - want '%v'", i, kmsKey, test.SealedDataKey)
|
||||||
|
}
|
||||||
|
if sealedKey.Algorithm != test.SealedKey.Algorithm {
|
||||||
|
t.Errorf("Test %d: seal algorithm mismatch: got '%s' - want '%s'", i, sealedKey.Algorithm, test.SealedKey.Algorithm)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) {
|
||||||
|
t.Errorf("Test %d: IV mismatch: got '%v' - want '%v'", i, sealedKey.IV, test.SealedKey.IV)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) {
|
||||||
|
t.Errorf("Test %d: sealed key mismatch: got '%v' - want '%v'", i, sealedKey.Key, test.SealedKey.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err == nil || err != logger.ErrCritical {
|
||||||
|
t.Errorf("Expected '%s' panic for invalid seal algorithm but got '%s'", logger.ErrCritical, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = S3.CreateMetadata(nil, "", []byte{}, SealedKey{Algorithm: InsecureSealAlgorithm})
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssecCreateMetadataTests = []SealedKey{
|
||||||
|
{Algorithm: SealAlgorithm},
|
||||||
|
{IV: [32]byte{0xff}, Key: [64]byte{0x7e}, Algorithm: SealAlgorithm},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECCreateMetadata(t *testing.T) {
|
||||||
|
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
||||||
|
logger.Disable = true
|
||||||
|
for i, test := range s3CreateMetadataTests {
|
||||||
|
metadata := SSEC.CreateMetadata(nil, test.SealedKey)
|
||||||
|
sealedKey, err := SSEC.ParseMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: failed to parse metadata: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sealedKey.Algorithm != test.SealedKey.Algorithm {
|
||||||
|
t.Errorf("Test %d: seal algorithm mismatch: got '%s' - want '%s'", i, sealedKey.Algorithm, test.SealedKey.Algorithm)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(sealedKey.IV[:], test.SealedKey.IV[:]) {
|
||||||
|
t.Errorf("Test %d: IV mismatch: got '%v' - want '%v'", i, sealedKey.IV, test.SealedKey.IV)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(sealedKey.Key[:], test.SealedKey.Key[:]) {
|
||||||
|
t.Errorf("Test %d: sealed key mismatch: got '%v' - want '%v'", i, sealedKey.Key, test.SealedKey.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err == nil || err != logger.ErrCritical {
|
||||||
|
t.Errorf("Expected '%s' panic for invalid seal algorithm but got '%s'", logger.ErrCritical, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = SSEC.CreateMetadata(nil, SealedKey{Algorithm: InsecureSealAlgorithm})
|
||||||
|
}
|
||||||
@@ -25,16 +25,54 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// SSEMultipart is the metadata key indicating that the object
|
||||||
|
// was uploaded using the S3 multipart API and stored using
|
||||||
|
// some from of server-side-encryption.
|
||||||
|
SSEMultipart = "X-Minio-Internal-Encrypted-Multipart"
|
||||||
|
|
||||||
|
// SSEIV is the metadata key referencing the random initialization
|
||||||
|
// vector (IV) used for SSE-S3 and SSE-C key derivation.
|
||||||
|
SSEIV = "X-Minio-Internal-Server-Side-Encryption-Iv"
|
||||||
|
|
||||||
|
// SSESealAlgorithm is the metadata key referencing the algorithm
|
||||||
|
// used by SSE-C and SSE-S3 to encrypt the object.
|
||||||
|
SSESealAlgorithm = "X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm"
|
||||||
|
|
||||||
|
// SSECSealedKey is the metadata key referencing the sealed object-key for SSE-C.
|
||||||
|
SSECSealedKey = "X-Minio-Internal-Server-Side-Encryption-Sealed-Key"
|
||||||
|
|
||||||
// S3SealedKey is the metadata key referencing the sealed object-key for SSE-S3.
|
// S3SealedKey is the metadata key referencing the sealed object-key for SSE-S3.
|
||||||
S3SealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key"
|
S3SealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key"
|
||||||
|
|
||||||
// S3KMSKeyID is the metadata key referencing the KMS key-id used to
|
// S3KMSKeyID is the metadata key referencing the KMS key-id used to
|
||||||
// generate/decrypt the S3-KMS-Sealed-Key. It is only used for SSE-S3 + KMS.
|
// generate/decrypt the S3-KMS-Sealed-Key. It is only used for SSE-S3 + KMS.
|
||||||
S3KMSKeyID = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id"
|
S3KMSKeyID = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id"
|
||||||
|
|
||||||
// S3KMSSealedKey is the metadata key referencing the encrypted key generated
|
// S3KMSSealedKey is the metadata key referencing the encrypted key generated
|
||||||
// by KMS. It is only used for SSE-S3 + KMS.
|
// by KMS. It is only used for SSE-S3 + KMS.
|
||||||
S3KMSSealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key"
|
S3KMSSealedKey = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SealAlgorithm is the encryption/sealing algorithm used to derive & seal
|
||||||
|
// the key-encryption-key and to en/decrypt the object data.
|
||||||
|
SealAlgorithm = "DAREv2-HMAC-SHA256"
|
||||||
|
|
||||||
|
// InsecureSealAlgorithm is the legacy encryption/sealing algorithm used
|
||||||
|
// to derive & seal the key-encryption-key and to en/decrypt the object data.
|
||||||
|
// This algorithm should not be used for new objects because its key derivation
|
||||||
|
// is not optimal. See: https://github.com/minio/minio/pull/6121
|
||||||
|
InsecureSealAlgorithm = "DARE-SHA256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the SSE domain as string. For SSE-S3 the
|
||||||
|
// domain is "SSE-S3".
|
||||||
|
func (s3) String() string { return "SSE-S3" }
|
||||||
|
|
||||||
|
// String returns the SSE domain as string. For SSE-C the
|
||||||
|
// domain is "SSE-C".
|
||||||
|
func (ssec) String() string { return "SSE-C" }
|
||||||
|
|
||||||
// EncryptSinglePart encrypts an io.Reader which must be the
|
// EncryptSinglePart encrypts an io.Reader which must be the
|
||||||
// the body of a single-part PUT request.
|
// the body of a single-part PUT request.
|
||||||
func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
||||||
@@ -45,6 +83,14 @@ func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EncryptMultiPart encrypts an io.Reader which must be the body of
|
||||||
|
// multi-part PUT request. It derives an unique encryption key from
|
||||||
|
// the partID and the object key.
|
||||||
|
func EncryptMultiPart(r io.Reader, partID int, key ObjectKey) io.Reader {
|
||||||
|
partKey := key.DerivePartKey(uint32(partID))
|
||||||
|
return EncryptSinglePart(r, ObjectKey(partKey))
|
||||||
|
}
|
||||||
|
|
||||||
// DecryptSinglePart decrypts an io.Writer which must an object
|
// DecryptSinglePart decrypts an io.Writer which must an object
|
||||||
// uploaded with the single-part PUT API. The offset and length
|
// uploaded with the single-part PUT API. The offset and length
|
||||||
// specify the requested range.
|
// specify the requested range.
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestS3String(t *testing.T) {
|
||||||
|
const Domain = "SSE-S3"
|
||||||
|
if domain := S3.String(); domain != Domain {
|
||||||
|
t.Errorf("S3's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSECString(t *testing.T) {
|
||||||
|
const Domain = "SSE-C"
|
||||||
|
if domain := SSEC.String(); domain != Domain {
|
||||||
|
t.Errorf("SSEC's string method returns wrong domain: got '%s' - want '%s'", domain, Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
vault "github.com/hashicorp/vault/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VaultEndpointEnv Vault endpoint environment variable
|
||||||
|
VaultEndpointEnv = "MINIO_SSE_VAULT_ENDPOINT"
|
||||||
|
// vaultAuthTypeEnv type of vault auth to be used
|
||||||
|
vaultAuthTypeEnv = "MINIO_SSE_VAULT_AUTH_TYPE"
|
||||||
|
// vaultAppRoleIDEnv Vault AppRole ID environment variable
|
||||||
|
vaultAppRoleIDEnv = "MINIO_SSE_VAULT_APPROLE_ID"
|
||||||
|
// vaultAppSecretIDEnv Vault AppRole Secret environment variable
|
||||||
|
vaultAppSecretIDEnv = "MINIO_SSE_VAULT_APPROLE_SECRET"
|
||||||
|
// vaultKeyVersionEnv Vault Key Version environment variable
|
||||||
|
vaultKeyVersionEnv = "MINIO_SSE_VAULT_KEY_VERSION"
|
||||||
|
// vaultKeyNameEnv Vault Encryption Key Name environment variable
|
||||||
|
vaultKeyNameEnv = "MINIO_SSE_VAULT_KEY_NAME"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//ErrKMSAuthLogin is raised when there is a failure authenticating to KMS
|
||||||
|
ErrKMSAuthLogin = errors.New("Vault service did not return auth info")
|
||||||
|
)
|
||||||
|
|
||||||
|
type vaultService struct {
|
||||||
|
config *VaultConfig
|
||||||
|
client *vault.Client
|
||||||
|
leaseDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// return transit secret engine's path for generate data key operation
|
||||||
|
func (v *vaultService) genDataKeyEndpoint(key string) string {
|
||||||
|
return "/transit/datakey/plaintext/" + key
|
||||||
|
}
|
||||||
|
|
||||||
|
// return transit secret engine's path for decrypt operation
|
||||||
|
func (v *vaultService) decryptEndpoint(key string) string {
|
||||||
|
return "/transit/decrypt/" + key
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultKey represents vault encryption key-id name & version
|
||||||
|
type VaultKey struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultAuth represents vault auth type to use. For now, AppRole is the only supported
|
||||||
|
// auth type.
|
||||||
|
type VaultAuth struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
AppRole VaultAppRole `json:"approle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultAppRole represents vault approle credentials
|
||||||
|
type VaultAppRole struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VaultConfig holds config required to start vault service
|
||||||
|
type VaultConfig struct {
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
Auth VaultAuth `json:"auth"`
|
||||||
|
Key VaultKey `json:"key-id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate whether all required env variables needed to start vault service have
|
||||||
|
// been set
|
||||||
|
func validateVaultConfig(c *VaultConfig) error {
|
||||||
|
if c.Endpoint == "" {
|
||||||
|
return fmt.Errorf("Missing hashicorp vault endpoint - %s is empty", VaultEndpointEnv)
|
||||||
|
}
|
||||||
|
if strings.ToLower(c.Auth.Type) != "approle" {
|
||||||
|
return fmt.Errorf("Unsupported hashicorp vault auth type - %s", vaultAuthTypeEnv)
|
||||||
|
}
|
||||||
|
if c.Auth.AppRole.ID == "" {
|
||||||
|
return fmt.Errorf("Missing hashicorp vault AppRole ID - %s is empty", vaultAppRoleIDEnv)
|
||||||
|
}
|
||||||
|
if c.Auth.AppRole.Secret == "" {
|
||||||
|
return fmt.Errorf("Missing hashicorp vault AppSecret ID - %s is empty", vaultAppSecretIDEnv)
|
||||||
|
}
|
||||||
|
if c.Key.Name == "" {
|
||||||
|
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyNameEnv)
|
||||||
|
}
|
||||||
|
if c.Key.Version < 0 {
|
||||||
|
return fmt.Errorf("Invalid value set in environment variable %s", vaultKeyVersionEnv)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate to vault with app role id and app role secret, and get a client access token, lease duration
|
||||||
|
func getVaultAccessToken(client *vault.Client, appRoleID, appSecret string) (token string, duration int, err error) {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"role_id": appRoleID,
|
||||||
|
"secret_id": appSecret,
|
||||||
|
}
|
||||||
|
resp, e := client.Logical().Write("auth/approle/login", data)
|
||||||
|
if e != nil {
|
||||||
|
return token, duration, e
|
||||||
|
}
|
||||||
|
if resp.Auth == nil {
|
||||||
|
return token, duration, ErrKMSAuthLogin
|
||||||
|
}
|
||||||
|
return resp.Auth.ClientToken, resp.Auth.LeaseDuration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVaultConfig sets KMSConfig from environment
|
||||||
|
// variables and performs validations.
|
||||||
|
func NewVaultConfig() (KMSConfig, error) {
|
||||||
|
kc := KMSConfig{}
|
||||||
|
endpoint := os.Getenv(VaultEndpointEnv)
|
||||||
|
roleID := os.Getenv(vaultAppRoleIDEnv)
|
||||||
|
roleSecret := os.Getenv(vaultAppSecretIDEnv)
|
||||||
|
keyName := os.Getenv(vaultKeyNameEnv)
|
||||||
|
keyVersion := 0
|
||||||
|
authType := "approle"
|
||||||
|
if versionStr := os.Getenv(vaultKeyVersionEnv); versionStr != "" {
|
||||||
|
version, err := strconv.Atoi(versionStr)
|
||||||
|
if err != nil {
|
||||||
|
return kc, fmt.Errorf("Unable to parse %s value (`%s`)", vaultKeyVersionEnv, versionStr)
|
||||||
|
}
|
||||||
|
keyVersion = version
|
||||||
|
}
|
||||||
|
// return if none of the vault env variables are configured
|
||||||
|
if (endpoint == "") && (roleID == "") && (roleSecret == "") && (keyName == "") && (keyVersion == 0) {
|
||||||
|
return kc, nil
|
||||||
|
}
|
||||||
|
c := VaultConfig{
|
||||||
|
Endpoint: endpoint,
|
||||||
|
Auth: VaultAuth{
|
||||||
|
Type: authType,
|
||||||
|
AppRole: VaultAppRole{
|
||||||
|
ID: roleID,
|
||||||
|
Secret: roleSecret,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Key: VaultKey{
|
||||||
|
Version: keyVersion,
|
||||||
|
Name: keyName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := validateVaultConfig(&c); err != nil {
|
||||||
|
return kc, err
|
||||||
|
}
|
||||||
|
kc.Vault = c
|
||||||
|
return kc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVault initializes Hashicorp Vault KMS by
|
||||||
|
// authenticating to Vault with the credentials in KMSConfig,
|
||||||
|
// and gets a client token for future api calls.
|
||||||
|
func NewVault(kmsConf KMSConfig) (KMS, error) {
|
||||||
|
config := kmsConf.Vault
|
||||||
|
c, err := vault.NewClient(&vault.Config{
|
||||||
|
Address: config.Endpoint,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
accessToken, leaseDuration, err := getVaultAccessToken(c, config.Auth.AppRole.ID, config.Auth.AppRole.Secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// authenticate and get the access token
|
||||||
|
c.SetToken(accessToken)
|
||||||
|
v := vaultService{client: c, config: &config, leaseDuration: time.Duration(leaseDuration)}
|
||||||
|
v.renewToken(c)
|
||||||
|
return &v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *vaultService) renewToken(c *vault.Client) {
|
||||||
|
retryDelay := 1 * time.Minute
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
s, err := c.Auth().Token().RenewSelf(int(v.leaseDuration))
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextRenew := s.Auth.LeaseDuration / 2
|
||||||
|
time.Sleep(time.Duration(nextRenew) * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a random plain text key, sealed plain text key from
|
||||||
|
// Vault. It returns the plaintext key and sealed plaintext key on success
|
||||||
|
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
|
||||||
|
contextStream := new(bytes.Buffer)
|
||||||
|
ctx.WriteTo(contextStream)
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
||||||
|
}
|
||||||
|
s, err1 := v.client.Logical().Write(v.genDataKeyEndpoint(keyID), payload)
|
||||||
|
|
||||||
|
if err1 != nil {
|
||||||
|
return key, sealedKey, err1
|
||||||
|
}
|
||||||
|
sealKey := s.Data["ciphertext"].(string)
|
||||||
|
plainKey, err := base64.StdEncoding.DecodeString(s.Data["plaintext"].(string))
|
||||||
|
if err != nil {
|
||||||
|
return key, sealedKey, err1
|
||||||
|
}
|
||||||
|
copy(key[:], []byte(plainKey))
|
||||||
|
return key, []byte(sealKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsealKMSKey unseals the sealedKey using the Vault master key
|
||||||
|
// referenced by the keyID. The plain text key is returned on success.
|
||||||
|
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
|
||||||
|
contextStream := new(bytes.Buffer)
|
||||||
|
ctx.WriteTo(contextStream)
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"ciphertext": string(sealedKey),
|
||||||
|
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
||||||
|
}
|
||||||
|
s, err1 := v.client.Logical().Write(v.decryptEndpoint(keyID), payload)
|
||||||
|
if err1 != nil {
|
||||||
|
return key, err1
|
||||||
|
}
|
||||||
|
base64Key := s.Data["plaintext"].(string)
|
||||||
|
plainKey, err1 := base64.StdEncoding.DecodeString(base64Key)
|
||||||
|
if err1 != nil {
|
||||||
|
return key, err1
|
||||||
|
}
|
||||||
|
copy(key[:], []byte(plainKey))
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
@@ -18,7 +18,11 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/minio/minio/pkg/ellipses"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CacheConfig represents cache config settings
|
// CacheConfig represents cache config settings
|
||||||
@@ -41,6 +45,15 @@ func (cfg *CacheConfig) UnmarshalJSON(data []byte) (err error) {
|
|||||||
if err = json.Unmarshal(data, _cfg); err != nil {
|
if err = json.Unmarshal(data, _cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _cfg.Expiry < 0 {
|
||||||
|
return errors.New("config expiry value should not be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _cfg.MaxUse < 0 {
|
||||||
|
return errors.New("config max use value should not be null or negative")
|
||||||
|
}
|
||||||
|
|
||||||
if _, err = parseCacheDrives(_cfg.Drives); err != nil {
|
if _, err = parseCacheDrives(_cfg.Drives); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -52,12 +65,42 @@ func (cfg *CacheConfig) UnmarshalJSON(data []byte) (err error) {
|
|||||||
|
|
||||||
// Parses given cacheDrivesEnv and returns a list of cache drives.
|
// Parses given cacheDrivesEnv and returns a list of cache drives.
|
||||||
func parseCacheDrives(drives []string) ([]string, error) {
|
func parseCacheDrives(drives []string) ([]string, error) {
|
||||||
|
if len(drives) == 0 {
|
||||||
|
return drives, nil
|
||||||
|
}
|
||||||
|
var endpoints []string
|
||||||
for _, d := range drives {
|
for _, d := range drives {
|
||||||
|
if ellipses.HasEllipses(d) {
|
||||||
|
s, err := parseCacheDrivePaths(d)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
endpoints = append(endpoints, s...)
|
||||||
|
} else {
|
||||||
|
endpoints = append(endpoints, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range endpoints {
|
||||||
if !filepath.IsAbs(d) {
|
if !filepath.IsAbs(d) {
|
||||||
return nil, uiErrInvalidCacheDrivesValue(nil).Msg("cache dir should be absolute path: %s", d)
|
return nil, uiErrInvalidCacheDrivesValue(nil).Msg("cache dir should be absolute path: %s", d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return drives, nil
|
return endpoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses all arguments and returns a slice of drive paths following the ellipses pattern.
|
||||||
|
func parseCacheDrivePaths(arg string) (ep []string, err error) {
|
||||||
|
patterns, perr := ellipses.FindEllipsesPatterns(arg)
|
||||||
|
if perr != nil {
|
||||||
|
return []string{}, uiErrInvalidCacheDrivesValue(nil).Msg(perr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, lbls := range patterns.Expand() {
|
||||||
|
ep = append(ep, strings.Join(lbls, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ep, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses given cacheExcludesEnv and returns a list of cache exclude patterns.
|
// Parses given cacheExcludesEnv and returns a list of cache exclude patterns.
|
||||||
|
|||||||
@@ -41,12 +41,32 @@ func TestParseCacheDrives(t *testing.T) {
|
|||||||
expectedPatterns []string
|
expectedPatterns []string
|
||||||
success bool
|
success bool
|
||||||
}{"C:/home/drive1;C:/home/drive2;C:/home/drive3", []string{"C:/home/drive1", "C:/home/drive2", "C:/home/drive3"}, true})
|
}{"C:/home/drive1;C:/home/drive2;C:/home/drive3", []string{"C:/home/drive1", "C:/home/drive2", "C:/home/drive3"}, true})
|
||||||
|
testCases = append(testCases, struct {
|
||||||
|
driveStr string
|
||||||
|
expectedPatterns []string
|
||||||
|
success bool
|
||||||
|
}{"C:/home/drive{1...3}", []string{"C:/home/drive1", "C:/home/drive2", "C:/home/drive3"}, true})
|
||||||
|
testCases = append(testCases, struct {
|
||||||
|
driveStr string
|
||||||
|
expectedPatterns []string
|
||||||
|
success bool
|
||||||
|
}{"C:/home/drive{1..3}", []string{}, false})
|
||||||
} else {
|
} else {
|
||||||
testCases = append(testCases, struct {
|
testCases = append(testCases, struct {
|
||||||
driveStr string
|
driveStr string
|
||||||
expectedPatterns []string
|
expectedPatterns []string
|
||||||
success bool
|
success bool
|
||||||
}{"/home/drive1;/home/drive2;/home/drive3", []string{"/home/drive1", "/home/drive2", "/home/drive3"}, true})
|
}{"/home/drive1;/home/drive2;/home/drive3", []string{"/home/drive1", "/home/drive2", "/home/drive3"}, true})
|
||||||
|
testCases = append(testCases, struct {
|
||||||
|
driveStr string
|
||||||
|
expectedPatterns []string
|
||||||
|
success bool
|
||||||
|
}{"/home/drive{1...3}", []string{"/home/drive1", "/home/drive2", "/home/drive3"}, true})
|
||||||
|
testCases = append(testCases, struct {
|
||||||
|
driveStr string
|
||||||
|
expectedPatterns []string
|
||||||
|
success bool
|
||||||
|
}{"/home/drive{1..3}", []string{}, false})
|
||||||
}
|
}
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
drives, err := parseCacheDrives(strings.Split(testCase.driveStr, cacheEnvDelimiter))
|
drives, err := parseCacheDrives(strings.Split(testCase.driveStr, cacheEnvDelimiter))
|
||||||
|
|||||||
+12
-24
@@ -32,14 +32,10 @@ import (
|
|||||||
"github.com/djherbis/atime"
|
"github.com/djherbis/atime"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/wildcard"
|
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
"github.com/minio/minio/pkg/wildcard"
|
||||||
)
|
)
|
||||||
|
|
||||||
// list of all errors that can be ignored in tree walk operation in disk cache
|
|
||||||
var cacheTreeWalkIgnoredErrs = append(baseIgnoredErrs, errDiskAccessDenied, errVolumeNotFound, errFileNotFound)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// disk cache needs to have cacheSizeMultiplier * object size space free for a cache entry to be created.
|
// disk cache needs to have cacheSizeMultiplier * object size space free for a cache entry to be created.
|
||||||
cacheSizeMultiplier = 100
|
cacheSizeMultiplier = 100
|
||||||
@@ -291,40 +287,32 @@ func (c cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string)
|
|||||||
// Returns function "listDir" of the type listDirFunc.
|
// Returns function "listDir" of the type listDirFunc.
|
||||||
// isLeaf - is used by listDir function to check if an entry is a leaf or non-leaf entry.
|
// isLeaf - is used by listDir function to check if an entry is a leaf or non-leaf entry.
|
||||||
// disks - list of fsObjects
|
// disks - list of fsObjects
|
||||||
func listDirCacheFactory(isLeaf isLeafFunc, treeWalkIgnoredErrs []error, disks []*cacheFSObjects) listDirFunc {
|
func listDirCacheFactory(isLeaf isLeafFunc, disks []*cacheFSObjects) listDirFunc {
|
||||||
listCacheDirs := func(bucket, prefixDir, prefixEntry string) (dirs []string, err error) {
|
listCacheDirs := func(bucket, prefixDir, prefixEntry string) (dirs []string) {
|
||||||
var entries []string
|
var entries []string
|
||||||
for _, disk := range disks {
|
for _, disk := range disks {
|
||||||
// ignore disk-caches that might be missing/offline
|
// ignore disk-caches that might be missing/offline
|
||||||
if disk == nil {
|
if disk == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fs := disk.FSObjects
|
|
||||||
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
|
||||||
|
|
||||||
// For any reason disk was deleted or goes offline, continue
|
fs := disk.FSObjects
|
||||||
// and list from other disks if possible.
|
var err error
|
||||||
|
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsErrIgnored(err, treeWalkIgnoredErrs...) {
|
continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter entries that have the prefix prefixEntry.
|
// Filter entries that have the prefix prefixEntry.
|
||||||
entries = filterMatchingPrefix(entries, prefixEntry)
|
entries = filterMatchingPrefix(entries, prefixEntry)
|
||||||
dirs = append(dirs, entries...)
|
dirs = append(dirs, entries...)
|
||||||
}
|
}
|
||||||
return dirs, nil
|
return dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
||||||
listDir := func(bucket, prefixDir, prefixEntry string) (mergedEntries []string, delayIsLeaf bool, err error) {
|
listDir := func(bucket, prefixDir, prefixEntry string) (mergedEntries []string, delayIsLeaf bool) {
|
||||||
var cacheEntries []string
|
cacheEntries := listCacheDirs(bucket, prefixDir, prefixEntry)
|
||||||
cacheEntries, err = listCacheDirs(bucket, prefixDir, prefixEntry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
for _, entry := range cacheEntries {
|
for _, entry := range cacheEntries {
|
||||||
// Find elements in entries which are not in mergedEntries
|
// Find elements in entries which are not in mergedEntries
|
||||||
idx := sort.SearchStrings(mergedEntries, entry)
|
idx := sort.SearchStrings(mergedEntries, entry)
|
||||||
@@ -335,7 +323,7 @@ func listDirCacheFactory(isLeaf isLeafFunc, treeWalkIgnoredErrs []error, disks [
|
|||||||
mergedEntries = append(mergedEntries, entry)
|
mergedEntries = append(mergedEntries, entry)
|
||||||
sort.Strings(mergedEntries)
|
sort.Strings(mergedEntries)
|
||||||
}
|
}
|
||||||
return mergedEntries, false, nil
|
return mergedEntries, false
|
||||||
}
|
}
|
||||||
return listDir
|
return listDir
|
||||||
}
|
}
|
||||||
@@ -371,7 +359,7 @@ func (c cacheObjects) listCacheObjects(ctx context.Context, bucket, prefix, mark
|
|||||||
return fs.isObjectDir(bucket, object)
|
return fs.isObjectDir(bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
listDir := listDirCacheFactory(isLeaf, cacheTreeWalkIgnoredErrs, c.cache.cfs)
|
listDir := listDirCacheFactory(isLeaf, c.cache.cfs)
|
||||||
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
|
walkResultCh = startTreeWalk(ctx, bucket, prefix, marker, recursive, listDir, isLeaf, isLeafDir, endWalkCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-15
@@ -19,7 +19,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -28,21 +27,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Initialize cache FS objects.
|
// Initialize cache FS objects.
|
||||||
func initCacheFSObjects(disk string, cacheMaxUse int, t *testing.T) (*cacheFSObjects, error) {
|
func initCacheFSObjects(disk string, cacheMaxUse int) (*cacheFSObjects, error) {
|
||||||
newTestConfig(globalMinioDefaultRegion)
|
return newCacheFSObjects(disk, globalCacheExpiry, cacheMaxUse)
|
||||||
var err error
|
|
||||||
obj, err := newCacheFSObjects(disk, globalCacheExpiry, cacheMaxUse)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return obj, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// inits diskCache struct for nDisks
|
// inits diskCache struct for nDisks
|
||||||
func initDiskCaches(drives []string, cacheMaxUse int, t *testing.T) (*diskCache, error) {
|
func initDiskCaches(drives []string, cacheMaxUse int, t *testing.T) (*diskCache, error) {
|
||||||
var cfs []*cacheFSObjects
|
var cfs []*cacheFSObjects
|
||||||
for _, d := range drives {
|
for _, d := range drives {
|
||||||
obj, err := initCacheFSObjects(d, cacheMaxUse, t)
|
obj, err := initCacheFSObjects(d, cacheMaxUse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -131,11 +124,6 @@ func TestGetCacheFSMaxUse(t *testing.T) {
|
|||||||
|
|
||||||
// test wildcard patterns for excluding entries from cache
|
// test wildcard patterns for excluding entries from cache
|
||||||
func TestCacheExclusion(t *testing.T) {
|
func TestCacheExclusion(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
fsDirs, err := getRandomDisks(1)
|
fsDirs, err := getRandomDisks(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
@@ -132,14 +131,6 @@ func (api *DummyObjectLayer) ListObjectsHeal(ctx context.Context, bucket, prefix
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DummyObjectLayer) ListLocks(ctx context.Context, bucket, prefix string, duration time.Duration) (info []VolumeLockInfo, err error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *DummyObjectLayer) ClearLocks(context.Context, []VolumeLockInfo) (err error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *DummyObjectLayer) SetBucketPolicy(context.Context, string, *policy.Policy) (err error) {
|
func (api *DummyObjectLayer) SetBucketPolicy(context.Context, string, *policy.Policy) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+288
-371
@@ -17,19 +17,18 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/md5"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/ioutil"
|
"github.com/minio/minio/pkg/ioutil"
|
||||||
sha256 "github.com/minio/sha256-simd"
|
sha256 "github.com/minio/sha256-simd"
|
||||||
@@ -40,34 +39,12 @@ var (
|
|||||||
// AWS errors for invalid SSE-C requests.
|
// AWS errors for invalid SSE-C requests.
|
||||||
errInsecureSSERequest = errors.New("SSE-C requests require TLS connections")
|
errInsecureSSERequest = errors.New("SSE-C requests require TLS connections")
|
||||||
errEncryptedObject = errors.New("The object was stored using a form of SSE")
|
errEncryptedObject = errors.New("The object was stored using a form of SSE")
|
||||||
errInvalidSSEAlgorithm = errors.New("The SSE-C algorithm is not valid")
|
|
||||||
errMissingSSEKey = errors.New("The SSE-C request is missing the customer key")
|
|
||||||
errInvalidSSEKey = errors.New("The SSE-C key is invalid")
|
|
||||||
errMissingSSEKeyMD5 = errors.New("The SSE-C request is missing the customer key MD5")
|
|
||||||
errSSEKeyMD5Mismatch = errors.New("The key MD5 does not match the SSE-C key")
|
|
||||||
errSSEKeyMismatch = errors.New("The SSE-C key is not correct") // access denied
|
|
||||||
errInvalidSSEParameters = errors.New("The SSE-C key for key-rotation is not correct") // special access denied
|
errInvalidSSEParameters = errors.New("The SSE-C key for key-rotation is not correct") // special access denied
|
||||||
|
errKMSNotConfigured = errors.New("KMS not configured for a server side encrypted object")
|
||||||
// Additional Minio errors for SSE-C requests.
|
// Additional Minio errors for SSE-C requests.
|
||||||
errObjectTampered = errors.New("The requested object was modified and may be compromised")
|
errObjectTampered = errors.New("The requested object was modified and may be compromised")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// SSECustomerAlgorithm is the AWS SSE-C algorithm HTTP header key.
|
|
||||||
SSECustomerAlgorithm = "X-Amz-Server-Side-Encryption-Customer-Algorithm"
|
|
||||||
// SSECustomerKey is the AWS SSE-C encryption key HTTP header key.
|
|
||||||
SSECustomerKey = "X-Amz-Server-Side-Encryption-Customer-Key"
|
|
||||||
// SSECustomerKeyMD5 is the AWS SSE-C encryption key MD5 HTTP header key.
|
|
||||||
SSECustomerKeyMD5 = "X-Amz-Server-Side-Encryption-Customer-Key-MD5"
|
|
||||||
|
|
||||||
// SSECopyCustomerAlgorithm is the AWS SSE-C algorithm HTTP header key for CopyObject API.
|
|
||||||
SSECopyCustomerAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm"
|
|
||||||
// SSECopyCustomerKey is the AWS SSE-C encryption key HTTP header key for CopyObject API.
|
|
||||||
SSECopyCustomerKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key"
|
|
||||||
// SSECopyCustomerKeyMD5 is the AWS SSE-C encryption key MD5 HTTP header key for CopyObject API.
|
|
||||||
SSECopyCustomerKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-MD5"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// SSECustomerKeySize is the size of valid client provided encryption keys in bytes.
|
// SSECustomerKeySize is the size of valid client provided encryption keys in bytes.
|
||||||
// Currently AWS supports only AES256. So the SSE-C key size is fixed to 32 bytes.
|
// Currently AWS supports only AES256. So the SSE-C key size is fixed to 32 bytes.
|
||||||
@@ -76,9 +53,6 @@ const (
|
|||||||
// SSEIVSize is the size of the IV data
|
// SSEIVSize is the size of the IV data
|
||||||
SSEIVSize = 32 // 32 bytes
|
SSEIVSize = 32 // 32 bytes
|
||||||
|
|
||||||
// SSECustomerAlgorithmAES256 the only valid S3 SSE-C encryption algorithm identifier.
|
|
||||||
SSECustomerAlgorithmAES256 = "AES256"
|
|
||||||
|
|
||||||
// SSE dare package block size.
|
// SSE dare package block size.
|
||||||
sseDAREPackageBlockSize = 64 * 1024 // 64KiB bytes
|
sseDAREPackageBlockSize = 64 * 1024 // 64KiB bytes
|
||||||
|
|
||||||
@@ -87,82 +61,26 @@ const (
|
|||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SSE-C key derivation, key verification and key update:
|
|
||||||
// H: Hash function [32 = |H(m)|]
|
|
||||||
// AE: authenticated encryption scheme, AD: authenticated decryption scheme [m = AD(k, AE(k, m))]
|
|
||||||
//
|
|
||||||
// Key derivation:
|
|
||||||
// Input:
|
|
||||||
// key := 32 bytes # client provided key
|
|
||||||
// Re, Rm := 32 bytes, 32 bytes # uniformly random
|
|
||||||
//
|
|
||||||
// Seal:
|
|
||||||
// k := H(key || Re) # object encryption key
|
|
||||||
// r := H(Rm) # save as object metadata [ServerSideEncryptionIV]
|
|
||||||
// KeK := H(key || r) # key encryption key
|
|
||||||
// K := AE(KeK, k) # save as object metadata [ServerSideEncryptionSealedKey]
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
|
||||||
// Key verification:
|
|
||||||
// Input:
|
|
||||||
// key := 32 bytes # client provided key
|
|
||||||
// r := 32 bytes # object metadata [ServerSideEncryptionIV]
|
|
||||||
// K := 32 bytes # object metadata [ServerSideEncryptionSealedKey]
|
|
||||||
//
|
|
||||||
// Open:
|
|
||||||
// KeK := H(key || r) # key encryption key
|
|
||||||
// k := AD(Kek, K) # object encryption key
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
// Key update:
|
|
||||||
// Input:
|
|
||||||
// key := 32 bytes # old client provided key
|
|
||||||
// key' := 32 bytes # new client provided key
|
|
||||||
// Rm := 32 bytes # uniformly random
|
|
||||||
// r := 32 bytes # object metadata [ServerSideEncryptionIV]
|
|
||||||
// K := 32 bytes # object metadata [ServerSideEncryptionSealedKey]
|
|
||||||
//
|
|
||||||
// Update:
|
|
||||||
// 1. open:
|
|
||||||
// KeK := H(key || r) # key encryption key
|
|
||||||
// k := AD(Kek, K) # object encryption key
|
|
||||||
// 2. seal:
|
|
||||||
// r' := H(Rm) # save as object metadata [ServerSideEncryptionIV]
|
|
||||||
// KeK' := H(key' || r') # new key encryption key
|
|
||||||
// K' := AE(KeK', k) # save as object metadata [ServerSideEncryptionSealedKey]
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ServerSideEncryptionIV is a 32 byte randomly generated IV used to derive an
|
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||||
// unique key encryption key from the client provided key. The combination of this value
|
// hash function. The key derivation of DARE-SHA256 is not optimal and does not include the object path.
|
||||||
// and the client-provided key MUST be unique.
|
// It is considered legacy and should not be used anymore.
|
||||||
ServerSideEncryptionIV = ReservedMetadataPrefix + "Server-Side-Encryption-Iv"
|
SSESealAlgorithmDareSha256 = "DARE-SHA256"
|
||||||
|
|
||||||
// ServerSideEncryptionSealAlgorithm identifies a combination of a cryptographic hash function and
|
// SSESealAlgorithmDareV2HmacSha256 specifies DAREv2 as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||||
// an authenticated en/decryption scheme to seal the object encryption key.
|
// hash function for the HMAC PRF.
|
||||||
ServerSideEncryptionSealAlgorithm = ReservedMetadataPrefix + "Server-Side-Encryption-Seal-Algorithm"
|
SSESealAlgorithmDareV2HmacSha256 = "DAREv2-HMAC-SHA256"
|
||||||
|
|
||||||
// ServerSideEncryptionSealedKey is the sealed object encryption key. The sealed key can be decrypted
|
|
||||||
// by the key encryption key derived from the client provided key and the server-side-encryption IV.
|
|
||||||
ServerSideEncryptionSealedKey = ReservedMetadataPrefix + "Server-Side-Encryption-Sealed-Key"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
|
// hasServerSideEncryptionHeader returns true if the given HTTP header
|
||||||
// hash function.
|
// contains server-side-encryption.
|
||||||
const SSESealAlgorithmDareSha256 = "DARE-SHA256"
|
func hasServerSideEncryptionHeader(header http.Header) bool {
|
||||||
|
return crypto.S3.IsRequested(header) || crypto.SSEC.IsRequested(header)
|
||||||
// hasSSECustomerHeader returns true if the given HTTP header
|
|
||||||
// contains server-side-encryption with customer provided key fields.
|
|
||||||
func hasSSECustomerHeader(header http.Header) bool {
|
|
||||||
return header.Get(SSECustomerAlgorithm) != "" || header.Get(SSECustomerKey) != "" || header.Get(SSECustomerKeyMD5) != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasSSECopyCustomerHeader returns true if the given HTTP header
|
|
||||||
// contains copy source server-side-encryption with customer provided key fields.
|
|
||||||
func hasSSECopyCustomerHeader(header http.Header) bool {
|
|
||||||
return header.Get(SSECopyCustomerAlgorithm) != "" || header.Get(SSECopyCustomerKey) != "" || header.Get(SSECopyCustomerKeyMD5) != ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseSSECopyCustomerRequest parses the SSE-C header fields of the provided request.
|
// ParseSSECopyCustomerRequest parses the SSE-C header fields of the provided request.
|
||||||
// It returns the client provided key on success.
|
// It returns the client provided key on success.
|
||||||
func ParseSSECopyCustomerRequest(r *http.Request) (key []byte, err error) {
|
func ParseSSECopyCustomerRequest(r *http.Request, metadata map[string]string) (key []byte, err error) {
|
||||||
if !globalIsSSL { // minio only supports HTTP or HTTPS requests not both at the same time
|
if !globalIsSSL { // minio only supports HTTP or HTTPS requests not both at the same time
|
||||||
// we cannot use r.TLS == nil here because Go's http implementation reflects on
|
// we cannot use r.TLS == nil here because Go's http implementation reflects on
|
||||||
// the net.Conn and sets the TLS field of http.Request only if it's an tls.Conn.
|
// the net.Conn and sets the TLS field of http.Request only if it's an tls.Conn.
|
||||||
@@ -170,36 +88,11 @@ func ParseSSECopyCustomerRequest(r *http.Request) (key []byte, err error) {
|
|||||||
// will always fail -> r.TLS is always nil even for TLS requests.
|
// will always fail -> r.TLS is always nil even for TLS requests.
|
||||||
return nil, errInsecureSSERequest
|
return nil, errInsecureSSERequest
|
||||||
}
|
}
|
||||||
header := r.Header
|
if crypto.S3.IsEncrypted(metadata) && crypto.SSECopy.IsRequested(r.Header) {
|
||||||
if algorithm := header.Get(SSECopyCustomerAlgorithm); algorithm != SSECustomerAlgorithmAES256 {
|
return nil, crypto.ErrIncompatibleEncryptionMethod
|
||||||
return nil, errInvalidSSEAlgorithm
|
|
||||||
}
|
}
|
||||||
if header.Get(SSECopyCustomerKey) == "" {
|
k, err := crypto.SSECopy.ParseHTTP(r.Header)
|
||||||
return nil, errMissingSSEKey
|
return k[:], err
|
||||||
}
|
|
||||||
if header.Get(SSECopyCustomerKeyMD5) == "" {
|
|
||||||
return nil, errMissingSSEKeyMD5
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err = base64.StdEncoding.DecodeString(header.Get(SSECopyCustomerKey))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errInvalidSSEKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(key) != SSECustomerKeySize {
|
|
||||||
return nil, errInvalidSSEKey
|
|
||||||
}
|
|
||||||
// Make sure we purged the keys from http headers by now.
|
|
||||||
header.Del(SSECopyCustomerKey)
|
|
||||||
|
|
||||||
keyMD5, err := base64.StdEncoding.DecodeString(header.Get(SSECopyCustomerKeyMD5))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errSSEKeyMD5Mismatch
|
|
||||||
}
|
|
||||||
if md5Sum := md5.Sum(key); !bytes.Equal(md5Sum[:], keyMD5) {
|
|
||||||
return nil, errSSEKeyMD5Mismatch
|
|
||||||
}
|
|
||||||
return key, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseSSECustomerRequest parses the SSE-C header fields of the provided request.
|
// ParseSSECustomerRequest parses the SSE-C header fields of the provided request.
|
||||||
@@ -218,209 +111,182 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
|||||||
// will always fail -> r.TLS is always nil even for TLS requests.
|
// will always fail -> r.TLS is always nil even for TLS requests.
|
||||||
return nil, errInsecureSSERequest
|
return nil, errInsecureSSERequest
|
||||||
}
|
}
|
||||||
if algorithm := header.Get(SSECustomerAlgorithm); algorithm != SSECustomerAlgorithmAES256 {
|
if crypto.S3.IsRequested(header) && crypto.SSEC.IsRequested(header) {
|
||||||
return nil, errInvalidSSEAlgorithm
|
return key, crypto.ErrIncompatibleEncryptionMethod
|
||||||
}
|
|
||||||
if header.Get(SSECustomerKey) == "" {
|
|
||||||
return nil, errMissingSSEKey
|
|
||||||
}
|
|
||||||
if header.Get(SSECustomerKeyMD5) == "" {
|
|
||||||
return nil, errMissingSSEKeyMD5
|
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err = base64.StdEncoding.DecodeString(header.Get(SSECustomerKey))
|
k, err := crypto.SSEC.ParseHTTP(header)
|
||||||
if err != nil {
|
return k[:], err
|
||||||
return nil, errInvalidSSEKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(key) != SSECustomerKeySize {
|
|
||||||
return nil, errInvalidSSEKey
|
|
||||||
}
|
|
||||||
// Make sure we purged the keys from http headers by now.
|
|
||||||
header.Del(SSECustomerKey)
|
|
||||||
|
|
||||||
keyMD5, err := base64.StdEncoding.DecodeString(header.Get(SSECustomerKeyMD5))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errSSEKeyMD5Mismatch
|
|
||||||
}
|
|
||||||
if md5Sum := md5.Sum(key); !bytes.Equal(md5Sum[:], keyMD5) {
|
|
||||||
return nil, errSSEKeyMD5Mismatch
|
|
||||||
}
|
|
||||||
return key, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function rotates old to new key.
|
// This function rotates old to new key.
|
||||||
func rotateKey(oldKey []byte, newKey []byte, metadata map[string]string) error {
|
func rotateKey(oldKey []byte, newKey []byte, bucket, object string, metadata map[string]string) error {
|
||||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
||||||
|
|
||||||
if metadata[ServerSideEncryptionSealAlgorithm] != SSESealAlgorithmDareSha256 { // currently DARE-SHA256 is the only option
|
switch {
|
||||||
|
default:
|
||||||
return errObjectTampered
|
return errObjectTampered
|
||||||
}
|
case crypto.SSEC.IsEncrypted(metadata):
|
||||||
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV])
|
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
|
||||||
if err != nil || len(iv) != SSEIVSize {
|
if err != nil {
|
||||||
return errObjectTampered
|
return err
|
||||||
}
|
|
||||||
sealedKey, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionSealedKey])
|
|
||||||
if err != nil || len(sealedKey) != 64 {
|
|
||||||
return errObjectTampered
|
|
||||||
}
|
|
||||||
|
|
||||||
sha := sha256.New() // derive key encryption key
|
|
||||||
sha.Write(oldKey)
|
|
||||||
sha.Write(iv)
|
|
||||||
keyEncryptionKey := sha.Sum(nil)
|
|
||||||
|
|
||||||
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
|
|
||||||
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
|
|
||||||
Key: keyEncryptionKey,
|
|
||||||
})
|
|
||||||
if n != 32 || err != nil { // Either the provided key does not match or the object was tampered.
|
|
||||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
|
|
||||||
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
|
|
||||||
}
|
}
|
||||||
return errSSEKeyMismatch // To provide strict AWS S3 compatibility we return: access denied.
|
|
||||||
}
|
|
||||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
|
|
||||||
return nil // we don't need to rotate keys if newKey == oldKey
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce := make([]byte, 32) // generate random values for key derivation
|
var objectKey crypto.ObjectKey
|
||||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
var extKey [32]byte
|
||||||
return err
|
copy(extKey[:], oldKey)
|
||||||
|
if err = objectKey.Unseal(extKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
|
||||||
|
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 {
|
||||||
|
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
|
||||||
|
}
|
||||||
|
return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied.
|
||||||
|
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm {
|
||||||
|
return nil // don't rotate on equal keys if seal algorithm is latest
|
||||||
|
}
|
||||||
|
copy(extKey[:], newKey)
|
||||||
|
sealedKey = objectKey.Seal(extKey, sealedKey.IV, crypto.SSEC.String(), bucket, object)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
niv := sha256.Sum256(nonce[:]) // derive key encryption key
|
|
||||||
sha = sha256.New()
|
|
||||||
sha.Write(newKey)
|
|
||||||
sha.Write(niv[:])
|
|
||||||
keyEncryptionKey = sha.Sum(nil)
|
|
||||||
|
|
||||||
sealedKeyW := bytes.NewBuffer(nil) // sealedKey := 16 byte header + 32 byte payload + 16 byte tag
|
|
||||||
n, err = sio.Encrypt(sealedKeyW, bytes.NewReader(objectEncryptionKey.Bytes()), sio.Config{
|
|
||||||
Key: keyEncryptionKey,
|
|
||||||
})
|
|
||||||
if n != 64 || err != nil {
|
|
||||||
return errors.New("failed to seal object encryption key") // if this happens there's a bug in the code (may panic ?)
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(niv[:])
|
|
||||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareSha256
|
|
||||||
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKeyW.Bytes())
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncryptMetadata(key []byte, metadata map[string]string) ([]byte, error) {
|
func newEncryptMetadata(key []byte, bucket, object string, metadata map[string]string, sseS3 bool) ([]byte, error) {
|
||||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
||||||
|
|
||||||
// security notice:
|
var sealedKey crypto.SealedKey
|
||||||
// - If the first 32 bytes of the random value are ever repeated under the same client-provided
|
if sseS3 {
|
||||||
// key the encrypted object will not be tamper-proof. [ P(coll) ~= 1 / 2^(256 / 2)]
|
if globalKMS == nil {
|
||||||
// - If the last 32 bytes of the random value are ever repeated under the same client-provided
|
return nil, errKMSNotConfigured
|
||||||
// key an adversary may be able to extract the object encryption key. This depends on the
|
}
|
||||||
// authenticated en/decryption scheme. The DARE format will generate an 8 byte nonce which must
|
key, encKey, err := globalKMS.GenerateKey(globalKMSKeyID, crypto.Context{bucket: path.Join(bucket, object)})
|
||||||
// be repeated in addition to reveal the object encryption key.
|
if err != nil {
|
||||||
// [ P(coll) ~= 1 / 2^((256 + 64) / 2) ]
|
return nil, err
|
||||||
nonce := make([]byte, 32+SSEIVSize) // generate random values for key derivation
|
}
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
||||||
return nil, err
|
objectKey := crypto.GenerateKey(key, rand.Reader)
|
||||||
|
sealedKey = objectKey.Seal(key, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, object)
|
||||||
|
crypto.S3.CreateMetadata(metadata, globalKMSKeyID, encKey, sealedKey)
|
||||||
|
return objectKey[:], nil
|
||||||
}
|
}
|
||||||
sha := sha256.New() // derive object encryption key
|
var extKey [32]byte
|
||||||
sha.Write(key)
|
copy(extKey[:], key)
|
||||||
sha.Write(nonce[:32])
|
objectKey := crypto.GenerateKey(extKey, rand.Reader)
|
||||||
objectEncryptionKey := sha.Sum(nil)
|
sealedKey = objectKey.Seal(extKey, crypto.GenerateIV(rand.Reader), crypto.SSEC.String(), bucket, object)
|
||||||
|
crypto.SSEC.CreateMetadata(metadata, sealedKey)
|
||||||
|
return objectKey[:], nil
|
||||||
|
|
||||||
iv := sha256.Sum256(nonce[32:]) // derive key encryption key
|
|
||||||
sha = sha256.New()
|
|
||||||
sha.Write(key)
|
|
||||||
sha.Write(iv[:])
|
|
||||||
keyEncryptionKey := sha.Sum(nil)
|
|
||||||
|
|
||||||
sealedKey := bytes.NewBuffer(nil) // sealedKey := 16 byte header + 32 byte payload + 16 byte tag
|
|
||||||
n, err := sio.Encrypt(sealedKey, bytes.NewReader(objectEncryptionKey), sio.Config{
|
|
||||||
Key: keyEncryptionKey,
|
|
||||||
})
|
|
||||||
if n != 64 || err != nil {
|
|
||||||
return nil, errors.New("failed to seal object encryption key") // if this happens there's a bug in the code (may panic ?)
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(iv[:])
|
|
||||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareSha256
|
|
||||||
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Bytes())
|
|
||||||
|
|
||||||
return objectEncryptionKey, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncryptReader(content io.Reader, key []byte, metadata map[string]string) (io.Reader, error) {
|
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string, sseS3 bool) (io.Reader, error) {
|
||||||
objectEncryptionKey, err := newEncryptMetadata(key, metadata)
|
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata, sseS3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey})
|
reader, err := sio.EncryptReader(content, sio.Config{Key: objectEncryptionKey[:], MinVersion: sio.Version20})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errInvalidSSEKey
|
return nil, crypto.ErrInvalidCustomerKey
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader, nil
|
return reader, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set new encryption metadata from http request headers for SSE-C and generated key from KMS in the case of
|
||||||
|
// SSE-S3
|
||||||
|
func setEncryptionMetadata(r *http.Request, bucket, object string, metadata map[string]string) (err error) {
|
||||||
|
var (
|
||||||
|
key []byte
|
||||||
|
)
|
||||||
|
if crypto.SSEC.IsRequested(r.Header) {
|
||||||
|
key, err = ParseSSECustomerRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = newEncryptMetadata(key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// EncryptRequest takes the client provided content and encrypts the data
|
// EncryptRequest takes the client provided content and encrypts the data
|
||||||
// with the client provided key. It also marks the object as client-side-encrypted
|
// with the client provided key. It also marks the object as client-side-encrypted
|
||||||
// and sets the correct headers.
|
// and sets the correct headers.
|
||||||
func EncryptRequest(content io.Reader, r *http.Request, metadata map[string]string) (io.Reader, error) {
|
func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, metadata map[string]string) (io.Reader, error) {
|
||||||
key, err := ParseSSECustomerRequest(r)
|
|
||||||
if err != nil {
|
var (
|
||||||
return nil, err
|
key []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if crypto.S3.IsRequested(r.Header) && crypto.SSEC.IsRequested(r.Header) {
|
||||||
|
return nil, crypto.ErrIncompatibleEncryptionMethod
|
||||||
}
|
}
|
||||||
return newEncryptReader(content, key, metadata)
|
if crypto.SSEC.IsRequested(r.Header) {
|
||||||
|
key, err = ParseSSECustomerRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptCopyRequest decrypts the object with the client provided key. It also removes
|
// DecryptCopyRequest decrypts the object with the client provided key. It also removes
|
||||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||||
func DecryptCopyRequest(client io.Writer, r *http.Request, metadata map[string]string) (io.WriteCloser, error) {
|
func DecryptCopyRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
|
||||||
key, err := ParseSSECopyCustomerRequest(r)
|
var (
|
||||||
if err != nil {
|
key []byte
|
||||||
return nil, err
|
err error
|
||||||
|
)
|
||||||
|
if crypto.SSECopy.IsRequested(r.Header) {
|
||||||
|
key, err = ParseSSECopyCustomerRequest(r, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delete(metadata, SSECopyCustomerKey) // make sure we do not save the key by accident
|
delete(metadata, crypto.SSECopyKey) // make sure we do not save the key by accident
|
||||||
return newDecryptWriter(client, key, 0, metadata)
|
return newDecryptWriter(client, key, bucket, object, 0, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decryptObjectInfo(key []byte, metadata map[string]string) ([]byte, error) {
|
func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||||
if metadata[ServerSideEncryptionSealAlgorithm] != SSESealAlgorithmDareSha256 { // currently DARE-SHA256 is the only option
|
switch {
|
||||||
|
default:
|
||||||
return nil, errObjectTampered
|
return nil, errObjectTampered
|
||||||
}
|
case crypto.S3.IsEncrypted(metadata):
|
||||||
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV])
|
if globalKMS == nil {
|
||||||
if err != nil || len(iv) != SSEIVSize {
|
return nil, errKMSNotConfigured
|
||||||
return nil, errObjectTampered
|
}
|
||||||
}
|
keyID, kmsKey, sealedKey, err := crypto.S3.ParseMetadata(metadata)
|
||||||
sealedKey, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionSealedKey])
|
|
||||||
if err != nil || len(sealedKey) != 64 {
|
|
||||||
return nil, errObjectTampered
|
|
||||||
}
|
|
||||||
|
|
||||||
sha := sha256.New() // derive key encryption key
|
if err != nil {
|
||||||
sha.Write(key)
|
return nil, err
|
||||||
sha.Write(iv)
|
}
|
||||||
keyEncryptionKey := sha.Sum(nil)
|
extKey, err := globalKMS.UnsealKey(keyID, kmsKey, crypto.Context{bucket: path.Join(bucket, object)})
|
||||||
|
if err != nil {
|
||||||
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
|
return nil, err
|
||||||
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
|
}
|
||||||
Key: keyEncryptionKey,
|
var objectKey crypto.ObjectKey
|
||||||
})
|
if err = objectKey.Unseal(extKey, sealedKey, crypto.S3.String(), bucket, object); err != nil {
|
||||||
if n != 32 || err != nil {
|
return nil, err
|
||||||
// Either the provided key does not match or the object was tampered.
|
}
|
||||||
// To provide strict AWS S3 compatibility we return: access denied.
|
return objectKey[:], nil
|
||||||
return nil, errSSEKeyMismatch
|
case crypto.SSEC.IsEncrypted(metadata):
|
||||||
|
var extKey [32]byte
|
||||||
|
copy(extKey[:], key)
|
||||||
|
sealedKey, err := crypto.SSEC.ParseMetadata(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var objectKey crypto.ObjectKey
|
||||||
|
if err = objectKey.Unseal(extKey, sealedKey, crypto.SSEC.String(), bucket, object); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return objectKey[:], nil
|
||||||
}
|
}
|
||||||
return objectEncryptionKey.Bytes(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDecryptWriter(client io.Writer, key []byte, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
func newDecryptWriter(client io.Writer, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||||
objectEncryptionKey, err := decryptObjectInfo(key, metadata)
|
objectEncryptionKey, err := decryptObjectInfo(key, bucket, object, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
||||||
}
|
}
|
||||||
return newDecryptWriterWithObjectKey(client, objectEncryptionKey, seqNumber, metadata)
|
return newDecryptWriterWithObjectKey(client, objectEncryptionKey, seqNumber, metadata)
|
||||||
}
|
}
|
||||||
@@ -431,31 +297,37 @@ func newDecryptWriterWithObjectKey(client io.Writer, objectEncryptionKey []byte,
|
|||||||
SequenceNumber: seqNumber,
|
SequenceNumber: seqNumber,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errInvalidSSEKey
|
return nil, crypto.ErrInvalidCustomerKey
|
||||||
}
|
}
|
||||||
|
delete(metadata, crypto.SSEIV)
|
||||||
delete(metadata, ServerSideEncryptionIV)
|
delete(metadata, crypto.SSESealAlgorithm)
|
||||||
delete(metadata, ServerSideEncryptionSealAlgorithm)
|
delete(metadata, crypto.SSECSealedKey)
|
||||||
delete(metadata, ServerSideEncryptionSealedKey)
|
delete(metadata, crypto.SSEMultipart)
|
||||||
delete(metadata, ReservedMetadataPrefix+"Encrypted-Multipart")
|
delete(metadata, crypto.S3SealedKey)
|
||||||
|
delete(metadata, crypto.S3KMSSealedKey)
|
||||||
|
delete(metadata, crypto.S3KMSKeyID)
|
||||||
return writer, nil
|
return writer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptRequestWithSequenceNumber decrypts the object with the client provided key. It also removes
|
// DecryptRequestWithSequenceNumber decrypts the object with the client provided key. It also removes
|
||||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||||
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||||
|
if crypto.S3.IsEncrypted(metadata) {
|
||||||
|
return newDecryptWriter(client, nil, bucket, object, seqNumber, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
key, err := ParseSSECustomerRequest(r)
|
key, err := ParseSSECustomerRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
delete(metadata, crypto.SSECKey) // make sure we do not save the key by accident
|
||||||
return newDecryptWriter(client, key, seqNumber, metadata)
|
return newDecryptWriter(client, key, bucket, object, seqNumber, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptRequest decrypts the object with the client provided key. It also removes
|
// DecryptRequest decrypts the object with client provided key for SSE-C and SSE-S3. It also removes
|
||||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
// the encryption metadata from the object and sets the correct headers.
|
||||||
func DecryptRequest(client io.Writer, r *http.Request, metadata map[string]string) (io.WriteCloser, error) {
|
func DecryptRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
|
||||||
return DecryptRequestWithSequenceNumber(client, r, 0, metadata)
|
return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptBlocksWriter - decrypts multipart parts, while implementing a io.Writer compatible interface.
|
// DecryptBlocksWriter - decrypts multipart parts, while implementing a io.Writer compatible interface.
|
||||||
@@ -469,9 +341,10 @@ type DecryptBlocksWriter struct {
|
|||||||
// Current part index
|
// Current part index
|
||||||
partIndex int
|
partIndex int
|
||||||
// Parts information
|
// Parts information
|
||||||
parts []objectPartInfo
|
parts []objectPartInfo
|
||||||
req *http.Request
|
req *http.Request
|
||||||
metadata map[string]string
|
bucket, object string
|
||||||
|
metadata map[string]string
|
||||||
|
|
||||||
partEncRelOffset int64
|
partEncRelOffset int64
|
||||||
|
|
||||||
@@ -489,17 +362,21 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
|||||||
var key []byte
|
var key []byte
|
||||||
var err error
|
var err error
|
||||||
if w.copySource {
|
if w.copySource {
|
||||||
w.req.Header.Set(SSECopyCustomerKey, w.customerKeyHeader)
|
if crypto.SSEC.IsEncrypted(w.metadata) {
|
||||||
key, err = ParseSSECopyCustomerRequest(w.req)
|
w.req.Header.Set(crypto.SSECopyKey, w.customerKeyHeader)
|
||||||
|
key, err = ParseSSECopyCustomerRequest(w.req, w.metadata)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
w.req.Header.Set(SSECustomerKey, w.customerKeyHeader)
|
if crypto.SSEC.IsEncrypted(w.metadata) {
|
||||||
key, err = ParseSSECustomerRequest(w.req)
|
w.req.Header.Set(crypto.SSECKey, w.customerKeyHeader)
|
||||||
|
key, err = ParseSSECustomerRequest(w.req)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
objectEncryptionKey, err := decryptObjectInfo(key, m)
|
objectEncryptionKey, err := decryptObjectInfo(key, w.bucket, w.object, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -513,9 +390,9 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
|||||||
|
|
||||||
// make sure we do not save the key by accident
|
// make sure we do not save the key by accident
|
||||||
if w.copySource {
|
if w.copySource {
|
||||||
delete(m, SSECopyCustomerKey)
|
delete(m, crypto.SSECopyKey)
|
||||||
} else {
|
} else {
|
||||||
delete(m, SSECustomerKey)
|
delete(m, crypto.SSECKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure to provide a NopCloser such that a Close
|
// make sure to provide a NopCloser such that a Close
|
||||||
@@ -594,29 +471,26 @@ func (w *DecryptBlocksWriter) Close() error {
|
|||||||
// DecryptAllBlocksCopyRequest - setup a struct which can decrypt many concatenated encrypted data
|
// DecryptAllBlocksCopyRequest - setup a struct which can decrypt many concatenated encrypted data
|
||||||
// parts information helps to know the boundaries of each encrypted data block, this function decrypts
|
// parts information helps to know the boundaries of each encrypted data block, this function decrypts
|
||||||
// all parts starting from part-1.
|
// all parts starting from part-1.
|
||||||
func DecryptAllBlocksCopyRequest(client io.Writer, r *http.Request, objInfo ObjectInfo) (io.WriteCloser, int64, error) {
|
func DecryptAllBlocksCopyRequest(client io.Writer, r *http.Request, bucket, object string, objInfo ObjectInfo) (io.WriteCloser, int64, error) {
|
||||||
w, _, size, err := DecryptBlocksRequest(client, r, 0, objInfo.Size, objInfo, true)
|
w, _, size, err := DecryptBlocksRequest(client, r, bucket, object, 0, objInfo.Size, objInfo, true)
|
||||||
return w, size, err
|
return w, size, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptBlocksRequest - setup a struct which can decrypt many concatenated encrypted data
|
// DecryptBlocksRequest - setup a struct which can decrypt many concatenated encrypted data
|
||||||
// parts information helps to know the boundaries of each encrypted data block.
|
// parts information helps to know the boundaries of each encrypted data block.
|
||||||
func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length int64, objInfo ObjectInfo, copySource bool) (io.WriteCloser, int64, int64, error) {
|
func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object string, startOffset, length int64, objInfo ObjectInfo, copySource bool) (io.WriteCloser, int64, int64, error) {
|
||||||
seqNumber, encStartOffset, encLength := getEncryptedStartOffset(startOffset, length)
|
var seqNumber uint32
|
||||||
|
var encStartOffset, encLength int64
|
||||||
|
|
||||||
// Encryption length cannot be bigger than the file size, if it is
|
if len(objInfo.Parts) == 0 || !crypto.IsMultiPart(objInfo.UserDefined) {
|
||||||
// which is allowed in AWS S3, we simply default to EncryptedSize().
|
seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
|
||||||
if encLength+encStartOffset > objInfo.EncryptedSize() {
|
|
||||||
encLength = objInfo.EncryptedSize() - encStartOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(objInfo.Parts) == 0 || !objInfo.IsEncryptedMultipart() {
|
|
||||||
var writer io.WriteCloser
|
var writer io.WriteCloser
|
||||||
var err error
|
var err error
|
||||||
if copySource {
|
if copySource {
|
||||||
writer, err = DecryptCopyRequest(client, r, objInfo.UserDefined)
|
writer, err = DecryptCopyRequest(client, r, bucket, object, objInfo.UserDefined)
|
||||||
} else {
|
} else {
|
||||||
writer, err = DecryptRequestWithSequenceNumber(client, r, seqNumber, objInfo.UserDefined)
|
writer, err = DecryptRequestWithSequenceNumber(client, r, bucket, object, seqNumber, objInfo.UserDefined)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
@@ -624,6 +498,7 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
|
|||||||
return writer, encStartOffset, encLength, nil
|
return writer, encStartOffset, encLength, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seqNumber, encStartOffset, encLength = getEncryptedMultipartsOffsetLength(startOffset, length, objInfo)
|
||||||
var partStartIndex int
|
var partStartIndex int
|
||||||
var partStartOffset = startOffset
|
var partStartOffset = startOffset
|
||||||
// Skip parts until final offset maps to a particular part offset.
|
// Skip parts until final offset maps to a particular part offset.
|
||||||
@@ -656,7 +531,9 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
|
|||||||
parts: objInfo.Parts,
|
parts: objInfo.Parts,
|
||||||
partIndex: partStartIndex,
|
partIndex: partStartIndex,
|
||||||
req: r,
|
req: r,
|
||||||
customerKeyHeader: r.Header.Get(SSECustomerKey),
|
bucket: bucket,
|
||||||
|
object: object,
|
||||||
|
customerKeyHeader: r.Header.Get(crypto.SSECKey),
|
||||||
copySource: copySource,
|
copySource: copySource,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,24 +544,76 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Purge all the encryption headers.
|
// Purge all the encryption headers.
|
||||||
delete(objInfo.UserDefined, ServerSideEncryptionIV)
|
delete(objInfo.UserDefined, crypto.SSEIV)
|
||||||
delete(objInfo.UserDefined, ServerSideEncryptionSealAlgorithm)
|
delete(objInfo.UserDefined, crypto.SSESealAlgorithm)
|
||||||
delete(objInfo.UserDefined, ServerSideEncryptionSealedKey)
|
delete(objInfo.UserDefined, crypto.SSECSealedKey)
|
||||||
delete(objInfo.UserDefined, ReservedMetadataPrefix+"Encrypted-Multipart")
|
delete(objInfo.UserDefined, crypto.SSEMultipart)
|
||||||
|
|
||||||
|
if crypto.S3.IsEncrypted(objInfo.UserDefined) {
|
||||||
|
delete(objInfo.UserDefined, crypto.S3SealedKey)
|
||||||
|
delete(objInfo.UserDefined, crypto.S3KMSKeyID)
|
||||||
|
delete(objInfo.UserDefined, crypto.S3KMSSealedKey)
|
||||||
|
}
|
||||||
if w.copySource {
|
if w.copySource {
|
||||||
w.customerKeyHeader = r.Header.Get(SSECopyCustomerKey)
|
w.customerKeyHeader = r.Header.Get(crypto.SSECopyKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.buildDecrypter(partStartIndex + 1); err != nil {
|
if err := w.buildDecrypter(w.parts[w.partIndex].Number); err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return w, encStartOffset, encLength, nil
|
return w, encStartOffset, encLength, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEncryptedStartOffset - fetch sequence number, encrypted start offset and encrypted length.
|
// getEncryptedMultipartsOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
|
||||||
func getEncryptedStartOffset(offset, length int64) (seqNumber uint32, encOffset int64, encLength int64) {
|
func getEncryptedMultipartsOffsetLength(offset, length int64, obj ObjectInfo) (uint32, int64, int64) {
|
||||||
|
|
||||||
|
// Calculate encrypted offset of a multipart object
|
||||||
|
computeEncOffset := func(off int64, obj ObjectInfo) (seqNumber uint32, encryptedOffset int64, err error) {
|
||||||
|
var curPartEndOffset uint64
|
||||||
|
var prevPartsEncSize int64
|
||||||
|
for _, p := range obj.Parts {
|
||||||
|
size, decErr := sio.DecryptedSize(uint64(p.Size))
|
||||||
|
if decErr != nil {
|
||||||
|
err = errObjectTampered // assign correct error type
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if off < int64(curPartEndOffset+size) {
|
||||||
|
seqNumber, encryptedOffset, _ = getEncryptedSinglePartOffsetLength(off-int64(curPartEndOffset), 1, obj)
|
||||||
|
encryptedOffset += int64(prevPartsEncSize)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
curPartEndOffset += size
|
||||||
|
prevPartsEncSize += p.Size
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the encrypted start offset corresponding to the plain offset
|
||||||
|
seqNumber, encStartOffset, _ := computeEncOffset(offset, obj)
|
||||||
|
// Calculate also the encrypted end offset corresponding to plain offset + plain length
|
||||||
|
_, encEndOffset, _ := computeEncOffset(offset+length-1, obj)
|
||||||
|
|
||||||
|
// encLength is the diff between encrypted end offset and encrypted start offset + one package size
|
||||||
|
// to ensure all encrypted data are covered
|
||||||
|
encLength := encEndOffset - encStartOffset + (64*1024 + 32)
|
||||||
|
|
||||||
|
// Calculate total size of all parts
|
||||||
|
var totalPartsLength int64
|
||||||
|
for _, p := range obj.Parts {
|
||||||
|
totalPartsLength += p.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set encLength to maximum possible value if it exceeded total parts size
|
||||||
|
if encLength+encStartOffset > totalPartsLength {
|
||||||
|
encLength = totalPartsLength - encStartOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
return seqNumber, encStartOffset, encLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEncryptedSinglePartOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
|
||||||
|
func getEncryptedSinglePartOffsetLength(offset, length int64, objInfo ObjectInfo) (seqNumber uint32, encOffset int64, encLength int64) {
|
||||||
onePkgSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
onePkgSize := int64(sseDAREPackageBlockSize + sseDAREPackageMetaSize)
|
||||||
|
|
||||||
seqNumber = uint32(offset / sseDAREPackageBlockSize)
|
seqNumber = uint32(offset / sseDAREPackageBlockSize)
|
||||||
@@ -702,55 +631,36 @@ func getEncryptedStartOffset(offset, length int64) (seqNumber uint32, encOffset
|
|||||||
encLength += onePkgSize
|
encLength += onePkgSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if encLength+encOffset > objInfo.EncryptedSize() {
|
||||||
|
encLength = objInfo.EncryptedSize() - encOffset
|
||||||
|
}
|
||||||
return seqNumber, encOffset, encLength
|
return seqNumber, encOffset, encLength
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEncryptedMultipart - is the encrypted content multiparted?
|
|
||||||
func (o *ObjectInfo) IsEncryptedMultipart() bool {
|
|
||||||
_, ok := o.UserDefined[ReservedMetadataPrefix+"Encrypted-Multipart"]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEncrypted returns true if the object is marked as encrypted.
|
|
||||||
func (o *ObjectInfo) IsEncrypted() bool {
|
|
||||||
if _, ok := o.UserDefined[ServerSideEncryptionIV]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, ok := o.UserDefined[ServerSideEncryptionSealAlgorithm]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, ok := o.UserDefined[ServerSideEncryptionSealedKey]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEncrypted returns true if the object is marked as encrypted.
|
|
||||||
func (li *ListPartsInfo) IsEncrypted() bool {
|
|
||||||
if _, ok := li.UserDefined[ServerSideEncryptionIV]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, ok := li.UserDefined[ServerSideEncryptionSealAlgorithm]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, ok := li.UserDefined[ServerSideEncryptionSealedKey]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecryptedSize returns the size of the object after decryption in bytes.
|
// DecryptedSize returns the size of the object after decryption in bytes.
|
||||||
// It returns an error if the object is not encrypted or marked as encrypted
|
// It returns an error if the object is not encrypted or marked as encrypted
|
||||||
// but has an invalid size.
|
// but has an invalid size.
|
||||||
func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
||||||
if !o.IsEncrypted() {
|
if !crypto.IsEncrypted(o.UserDefined) {
|
||||||
return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
|
return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
|
||||||
}
|
}
|
||||||
size, err := sio.DecryptedSize(uint64(o.Size))
|
if len(o.Parts) == 0 || !crypto.IsMultiPart(o.UserDefined) {
|
||||||
if err != nil {
|
size, err := sio.DecryptedSize(uint64(o.Size))
|
||||||
err = errObjectTampered // assign correct error type
|
if err != nil {
|
||||||
|
err = errObjectTampered // assign correct error type
|
||||||
|
}
|
||||||
|
return int64(size), err
|
||||||
}
|
}
|
||||||
return int64(size), err
|
|
||||||
|
var size int64
|
||||||
|
for _, part := range o.Parts {
|
||||||
|
partSize, err := sio.DecryptedSize(uint64(part.Size))
|
||||||
|
if err != nil {
|
||||||
|
return 0, errObjectTampered
|
||||||
|
}
|
||||||
|
size += int64(partSize)
|
||||||
|
}
|
||||||
|
return size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptedSize returns the size of the object after encryption.
|
// EncryptedSize returns the size of the object after encryption.
|
||||||
@@ -780,10 +690,11 @@ func DecryptCopyObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErr
|
|||||||
if info.IsDir {
|
if info.IsDir {
|
||||||
return ErrNone, false
|
return ErrNone, false
|
||||||
}
|
}
|
||||||
if apiErr, encrypted = ErrNone, info.IsEncrypted(); !encrypted && hasSSECopyCustomerHeader(headers) {
|
if apiErr, encrypted = ErrNone, crypto.IsEncrypted(info.UserDefined); !encrypted && crypto.SSECopy.IsRequested(headers) {
|
||||||
apiErr = ErrInvalidEncryptionParameters
|
apiErr = ErrInvalidEncryptionParameters
|
||||||
} else if encrypted {
|
} else if encrypted {
|
||||||
if !hasSSECopyCustomerHeader(headers) {
|
if (!crypto.SSECopy.IsRequested(headers) && crypto.SSEC.IsEncrypted(info.UserDefined)) ||
|
||||||
|
(crypto.SSECopy.IsRequested(headers) && crypto.S3.IsEncrypted(info.UserDefined)) {
|
||||||
apiErr = ErrSSEEncryptedObject
|
apiErr = ErrSSEEncryptedObject
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -807,10 +718,16 @@ func DecryptObjectInfo(info *ObjectInfo, headers http.Header) (apiErr APIErrorCo
|
|||||||
if info.IsDir {
|
if info.IsDir {
|
||||||
return ErrNone, false
|
return ErrNone, false
|
||||||
}
|
}
|
||||||
if apiErr, encrypted = ErrNone, info.IsEncrypted(); !encrypted && hasSSECustomerHeader(headers) {
|
// disallow X-Amz-Server-Side-Encryption header on HEAD and GET
|
||||||
|
if crypto.S3.IsRequested(headers) {
|
||||||
|
apiErr = ErrInvalidEncryptionParameters
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if apiErr, encrypted = ErrNone, crypto.IsEncrypted(info.UserDefined); !encrypted && crypto.SSEC.IsRequested(headers) {
|
||||||
apiErr = ErrInvalidEncryptionParameters
|
apiErr = ErrInvalidEncryptionParameters
|
||||||
} else if encrypted {
|
} else if encrypted {
|
||||||
if !hasSSECustomerHeader(headers) {
|
if (crypto.SSEC.IsEncrypted(info.UserDefined) && !crypto.SSEC.IsRequested(headers)) ||
|
||||||
|
(crypto.S3.IsEncrypted(info.UserDefined) && crypto.SSEC.IsRequested(headers)) {
|
||||||
apiErr = ErrSSEEncryptedObject
|
apiErr = ErrSSEEncryptedObject
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+254
-150
@@ -18,21 +18,52 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var hasServerSideEncryptionHeaderTests = []struct {
|
||||||
|
headers map[string]string
|
||||||
|
sseRequest bool
|
||||||
|
}{
|
||||||
|
{headers: map[string]string{crypto.SSECAlgorithm: "AES256", crypto.SSECKey: "key", crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 0
|
||||||
|
{headers: map[string]string{crypto.SSECAlgorithm: "AES256"}, sseRequest: true}, // 1
|
||||||
|
{headers: map[string]string{crypto.SSECKey: "key"}, sseRequest: true}, // 2
|
||||||
|
{headers: map[string]string{crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||||
|
{headers: map[string]string{}, sseRequest: false}, // 4
|
||||||
|
{headers: map[string]string{crypto.SSECopyAlgorithm + " ": "AES256", " " + crypto.SSECopyKey: "key", crypto.SSECopyKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
||||||
|
{headers: map[string]string{crypto.SSECopyAlgorithm: "", crypto.SSECopyKey: "", crypto.SSECopyKeyMD5: ""}, sseRequest: false}, // 6
|
||||||
|
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: true}, // 6
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasServerSideEncryptionHeader(t *testing.T) {
|
||||||
|
for i, test := range hasServerSideEncryptionHeaderTests {
|
||||||
|
headers := http.Header{}
|
||||||
|
for k, v := range test.headers {
|
||||||
|
headers.Set(k, v)
|
||||||
|
}
|
||||||
|
if hasServerSideEncryptionHeader(headers) != test.sseRequest {
|
||||||
|
t.Errorf("Test %d: Expected hasServerSideEncryptionHeader to return %v", i, test.sseRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var hasSSECopyCustomerHeaderTests = []struct {
|
var hasSSECopyCustomerHeaderTests = []struct {
|
||||||
headers map[string]string
|
headers map[string]string
|
||||||
sseRequest bool
|
sseRequest bool
|
||||||
}{
|
}{
|
||||||
{headers: map[string]string{SSECopyCustomerAlgorithm: "AES256", SSECopyCustomerKey: "key", SSECopyCustomerKeyMD5: "md5"}, sseRequest: true}, // 0
|
{headers: map[string]string{crypto.SSECopyAlgorithm: "AES256", crypto.SSECopyKey: "key", crypto.SSECopyKeyMD5: "md5"}, sseRequest: true}, // 0
|
||||||
{headers: map[string]string{SSECopyCustomerAlgorithm: "AES256"}, sseRequest: true}, // 1
|
{headers: map[string]string{crypto.SSECopyAlgorithm: "AES256"}, sseRequest: true}, // 1
|
||||||
{headers: map[string]string{SSECopyCustomerKey: "key"}, sseRequest: true}, // 2
|
{headers: map[string]string{crypto.SSECopyKey: "key"}, sseRequest: true}, // 2
|
||||||
{headers: map[string]string{SSECopyCustomerKeyMD5: "md5"}, sseRequest: true}, // 3
|
{headers: map[string]string{crypto.SSECopyKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||||
{headers: map[string]string{}, sseRequest: false}, // 4
|
{headers: map[string]string{}, sseRequest: false}, // 4
|
||||||
{headers: map[string]string{SSECopyCustomerAlgorithm + " ": "AES256", " " + SSECopyCustomerKey: "key", SSECopyCustomerKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
{headers: map[string]string{crypto.SSECopyAlgorithm + " ": "AES256", " " + crypto.SSECopyKey: "key", crypto.SSECopyKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
||||||
{headers: map[string]string{SSECopyCustomerAlgorithm: "", SSECopyCustomerKey: "", SSECopyCustomerKeyMD5: ""}, sseRequest: false}, // 6
|
{headers: map[string]string{crypto.SSECopyAlgorithm: "", crypto.SSECopyKey: "", crypto.SSECopyKeyMD5: ""}, sseRequest: true}, // 6
|
||||||
|
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: false}, // 7
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsSSECopyCustomerRequest(t *testing.T) {
|
func TestIsSSECopyCustomerRequest(t *testing.T) {
|
||||||
@@ -41,8 +72,8 @@ func TestIsSSECopyCustomerRequest(t *testing.T) {
|
|||||||
for k, v := range test.headers {
|
for k, v := range test.headers {
|
||||||
headers.Set(k, v)
|
headers.Set(k, v)
|
||||||
}
|
}
|
||||||
if hasSSECopyCustomerHeader(headers) != test.sseRequest {
|
if crypto.SSECopy.IsRequested(headers) != test.sseRequest {
|
||||||
t.Errorf("Test %d: Expected hasSSECopyCustomerHeader to return %v", i, test.sseRequest)
|
t.Errorf("Test %d: Expected crypto.SSECopy.IsRequested to return %v", i, test.sseRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,13 +82,15 @@ var hasSSECustomerHeaderTests = []struct {
|
|||||||
headers map[string]string
|
headers map[string]string
|
||||||
sseRequest bool
|
sseRequest bool
|
||||||
}{
|
}{
|
||||||
{headers: map[string]string{SSECustomerAlgorithm: "AES256", SSECustomerKey: "key", SSECustomerKeyMD5: "md5"}, sseRequest: true}, // 0
|
{headers: map[string]string{crypto.SSECAlgorithm: "AES256", crypto.SSECKey: "key", crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 0
|
||||||
{headers: map[string]string{SSECustomerAlgorithm: "AES256"}, sseRequest: true}, // 1
|
{headers: map[string]string{crypto.SSECAlgorithm: "AES256"}, sseRequest: true}, // 1
|
||||||
{headers: map[string]string{SSECustomerKey: "key"}, sseRequest: true}, // 2
|
{headers: map[string]string{crypto.SSECKey: "key"}, sseRequest: true}, // 2
|
||||||
{headers: map[string]string{SSECustomerKeyMD5: "md5"}, sseRequest: true}, // 3
|
{headers: map[string]string{crypto.SSECKeyMD5: "md5"}, sseRequest: true}, // 3
|
||||||
{headers: map[string]string{}, sseRequest: false}, // 4
|
{headers: map[string]string{}, sseRequest: false}, // 4
|
||||||
{headers: map[string]string{SSECustomerAlgorithm + " ": "AES256", " " + SSECustomerKey: "key", SSECustomerKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
{headers: map[string]string{crypto.SSECAlgorithm + " ": "AES256", " " + crypto.SSECKey: "key", crypto.SSECKeyMD5 + " ": "md5"}, sseRequest: false}, // 5
|
||||||
{headers: map[string]string{SSECustomerAlgorithm: "", SSECustomerKey: "", SSECustomerKeyMD5: ""}, sseRequest: false}, // 6
|
{headers: map[string]string{crypto.SSECAlgorithm: "", crypto.SSECKey: "", crypto.SSECKeyMD5: ""}, sseRequest: false}, // 6
|
||||||
|
{headers: map[string]string{crypto.SSEHeader: ""}, sseRequest: false}, // 7
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TesthasSSECustomerHeader(t *testing.T) {
|
func TesthasSSECustomerHeader(t *testing.T) {
|
||||||
@@ -66,7 +99,7 @@ func TesthasSSECustomerHeader(t *testing.T) {
|
|||||||
for k, v := range test.headers {
|
for k, v := range test.headers {
|
||||||
headers.Set(k, v)
|
headers.Set(k, v)
|
||||||
}
|
}
|
||||||
if hasSSECustomerHeader(headers) != test.sseRequest {
|
if crypto.SSEC.IsRequested(headers) != test.sseRequest {
|
||||||
t.Errorf("Test %d: Expected hasSSECustomerHeader to return %v", i, test.sseRequest)
|
t.Errorf("Test %d: Expected hasSSECustomerHeader to return %v", i, test.sseRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,75 +112,84 @@ var parseSSECustomerRequestTests = []struct {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
||||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: nil,
|
useTLS: true, err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
||||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
useTLS: false, err: errInsecureSSERequest,
|
useTLS: false, err: errInsecureSSERequest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES 256",
|
crypto.SSECAlgorithm: "AES 256",
|
||||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
||||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errInvalidSSEAlgorithm,
|
useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
||||||
SSECustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
crypto.SSECKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
||||||
SSECustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
crypto.SSECKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errInvalidSSEKey,
|
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
crypto.SSECKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
||||||
SSECustomerKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
crypto.SSECKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
||||||
SSECustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errInvalidSSEKey,
|
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "", // 7
|
crypto.SSECKey: "", // 7
|
||||||
SSECustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
crypto.SSECKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errMissingSSEKey,
|
useTLS: true, err: crypto.ErrMissingCustomerKey,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
||||||
SSECustomerKeyMD5: "",
|
crypto.SSECKeyMD5: "",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errMissingSSEKeyMD5,
|
useTLS: true, err: crypto.ErrMissingCustomerKeyMD5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: map[string]string{
|
||||||
|
crypto.SSECAlgorithm: "AES256",
|
||||||
|
crypto.SSECKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
||||||
|
crypto.SSECKeyMD5: "",
|
||||||
|
crypto.SSEHeader: "",
|
||||||
|
},
|
||||||
|
useTLS: true, err: crypto.ErrIncompatibleEncryptionMethod,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,8 +208,8 @@ func TestParseSSECustomerRequest(t *testing.T) {
|
|||||||
if err != test.err {
|
if err != test.err {
|
||||||
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
||||||
}
|
}
|
||||||
key := request.Header.Get(SSECustomerKey)
|
key := request.Header.Get(crypto.SSECKey)
|
||||||
if (err == nil || err == errSSEKeyMD5Mismatch) && key != "" {
|
if (err == nil || err == crypto.ErrCustomerKeyMD5Mismatch) && key != "" {
|
||||||
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
|
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,81 +217,100 @@ func TestParseSSECustomerRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var parseSSECopyCustomerRequestTests = []struct {
|
var parseSSECopyCustomerRequestTests = []struct {
|
||||||
headers map[string]string
|
headers map[string]string
|
||||||
useTLS bool
|
metadata map[string]string
|
||||||
err error
|
useTLS bool
|
||||||
|
err error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECopyCustomerAlgorithm: "AES256",
|
crypto.SSECopyAlgorithm: "AES256",
|
||||||
SSECopyCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
||||||
SSECopyCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: nil,
|
metadata: map[string]string{},
|
||||||
|
useTLS: true, err: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECopyCustomerAlgorithm: "AES256",
|
crypto.SSECopyAlgorithm: "AES256",
|
||||||
SSECopyCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 0
|
||||||
SSECopyCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
useTLS: false, err: errInsecureSSERequest,
|
metadata: map[string]string{"X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key": base64.StdEncoding.EncodeToString(make([]byte, 64))},
|
||||||
|
useTLS: true, err: crypto.ErrIncompatibleEncryptionMethod,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECopyCustomerAlgorithm: "AES 256",
|
crypto.SSECopyAlgorithm: "AES256",
|
||||||
SSECopyCustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 1
|
||||||
SSECopyCustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errInvalidSSEAlgorithm,
|
metadata: map[string]string{},
|
||||||
|
useTLS: false, err: errInsecureSSERequest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECopyCustomerAlgorithm: "AES256",
|
crypto.SSECopyAlgorithm: "AES 256",
|
||||||
SSECopyCustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
crypto.SSECopyKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=", // 2
|
||||||
SSECopyCustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
crypto.SSECopyKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
metadata: map[string]string{},
|
||||||
|
useTLS: true, err: crypto.ErrInvalidCustomerAlgorithm,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECopyCustomerAlgorithm: "AES256",
|
crypto.SSECopyAlgorithm: "AES256",
|
||||||
SSECopyCustomerKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 3
|
||||||
SSECopyCustomerKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errInvalidSSEKey,
|
metadata: map[string]string{},
|
||||||
|
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECopyCustomerAlgorithm: "AES256",
|
crypto.SSECopyAlgorithm: "AES256",
|
||||||
SSECopyCustomerKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
crypto.SSECopyKey: " jE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 4
|
||||||
SSECopyCustomerKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
crypto.SSECopyKeyMD5: "H+jq/LwEOEO90YtiTuNFVw==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errSSEKeyMD5Mismatch,
|
metadata: map[string]string{},
|
||||||
|
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECopyCustomerAlgorithm: "AES256",
|
crypto.SSECopyAlgorithm: "AES256",
|
||||||
SSECopyCustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
crypto.SSECopyKey: "NjE0SL87s+ZhYtaTrg5eI5cjhCQLGPVMKenPG2bCJFw=", // 5
|
||||||
SSECopyCustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
crypto.SSECopyKeyMD5: " +jq/LwEOEO90YtiTuNFVw==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errInvalidSSEKey,
|
metadata: map[string]string{},
|
||||||
|
useTLS: true, err: crypto.ErrCustomerKeyMD5Mismatch,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECopyCustomerAlgorithm: "AES256",
|
crypto.SSECopyAlgorithm: "AES256",
|
||||||
SSECopyCustomerKey: "", // 7
|
crypto.SSECopyKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 6
|
||||||
SSECopyCustomerKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errMissingSSEKey,
|
metadata: map[string]string{},
|
||||||
|
useTLS: true, err: crypto.ErrInvalidCustomerKey,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: map[string]string{
|
headers: map[string]string{
|
||||||
SSECopyCustomerAlgorithm: "AES256",
|
crypto.SSECopyAlgorithm: "AES256",
|
||||||
SSECopyCustomerKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
crypto.SSECopyKey: "", // 7
|
||||||
SSECopyCustomerKeyMD5: "",
|
crypto.SSECopyKeyMD5: "9KPgDdZNTHimuYCwnJTp5g==",
|
||||||
},
|
},
|
||||||
useTLS: true, err: errMissingSSEKeyMD5,
|
metadata: map[string]string{},
|
||||||
|
useTLS: true, err: crypto.ErrMissingCustomerKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: map[string]string{
|
||||||
|
crypto.SSECopyAlgorithm: "AES256",
|
||||||
|
crypto.SSECopyKey: "vFQ9ScFOF6Tu/BfzMS+rVMvlZGJHi5HmGJenJfrfKI45", // 8
|
||||||
|
crypto.SSECopyKeyMD5: "",
|
||||||
|
},
|
||||||
|
metadata: map[string]string{},
|
||||||
|
useTLS: true, err: crypto.ErrMissingCustomerKeyMD5,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,12 +325,12 @@ func TestParseSSECopyCustomerRequest(t *testing.T) {
|
|||||||
request.Header = headers
|
request.Header = headers
|
||||||
globalIsSSL = test.useTLS
|
globalIsSSL = test.useTLS
|
||||||
|
|
||||||
_, err := ParseSSECopyCustomerRequest(request)
|
_, err := ParseSSECopyCustomerRequest(request, test.metadata)
|
||||||
if err != test.err {
|
if err != test.err {
|
||||||
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
t.Errorf("Test %d: Parse returned: %v want: %v", i, err, test.err)
|
||||||
}
|
}
|
||||||
key := request.Header.Get(SSECopyCustomerKey)
|
key := request.Header.Get(crypto.SSECopyKey)
|
||||||
if (err == nil || err == errSSEKeyMD5Mismatch) && key != "" {
|
if (err == nil || err == crypto.ErrCustomerKeyMD5Mismatch) && key != "" {
|
||||||
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
|
t.Errorf("Test %d: Client key survived parsing - found key: %v", i, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,20 +342,20 @@ var encryptRequestTests = []struct {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
header: map[string]string{
|
header: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{},
|
metadata: map[string]string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: map[string]string{
|
header: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -308,79 +369,119 @@ func TestEncryptRequest(t *testing.T) {
|
|||||||
for k, v := range test.header {
|
for k, v := range test.header {
|
||||||
req.Header.Set(k, v)
|
req.Header.Set(k, v)
|
||||||
}
|
}
|
||||||
_, err := EncryptRequest(content, req, test.metadata)
|
_, err := EncryptRequest(content, req, "bucket", "object", test.metadata)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||||
}
|
}
|
||||||
if key, ok := test.metadata[SSECustomerKey]; ok {
|
if key, ok := test.metadata[crypto.SSECKey]; ok {
|
||||||
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
||||||
}
|
}
|
||||||
if kdf, ok := test.metadata[ServerSideEncryptionSealAlgorithm]; !ok {
|
if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; !ok {
|
||||||
t.Errorf("Test %d: ServerSideEncryptionKDF must be part of metadata: %v", i, kdf)
|
t.Errorf("Test %d: ServerSideEncryptionKDF must be part of metadata: %v", i, kdf)
|
||||||
}
|
}
|
||||||
if iv, ok := test.metadata[ServerSideEncryptionIV]; !ok {
|
if iv, ok := test.metadata[crypto.SSEIV]; !ok {
|
||||||
t.Errorf("Test %d: ServerSideEncryptionIV must be part of metadata: %v", i, iv)
|
t.Errorf("Test %d: crypto.SSEIV must be part of metadata: %v", i, iv)
|
||||||
}
|
}
|
||||||
if mac, ok := test.metadata[ServerSideEncryptionSealedKey]; !ok {
|
if mac, ok := test.metadata[crypto.SSECSealedKey]; !ok {
|
||||||
t.Errorf("Test %d: ServerSideEncryptionKeyMAC must be part of metadata: %v", i, mac)
|
t.Errorf("Test %d: ServerSideEncryptionKeyMAC must be part of metadata: %v", i, mac)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var decryptRequestTests = []struct {
|
var decryptRequestTests = []struct {
|
||||||
header map[string]string
|
bucket, object string
|
||||||
metadata map[string]string
|
header map[string]string
|
||||||
shouldFail bool
|
metadata map[string]string
|
||||||
|
shouldFail bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
bucket: "bucket",
|
||||||
|
object: "object",
|
||||||
header: map[string]string{
|
header: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||||
SSECustomerKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
||||||
ServerSideEncryptionIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=",
|
crypto.SSEIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=",
|
||||||
ServerSideEncryptionSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==",
|
crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==",
|
||||||
},
|
},
|
||||||
shouldFail: false,
|
shouldFail: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
bucket: "bucket",
|
||||||
|
object: "object",
|
||||||
header: map[string]string{
|
header: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
ServerSideEncryptionSealAlgorithm: "HMAC-SHA3",
|
crypto.SSESealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
||||||
ServerSideEncryptionIV: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||||
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||||
|
},
|
||||||
|
shouldFail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bucket: "bucket",
|
||||||
|
object: "object",
|
||||||
|
header: map[string]string{
|
||||||
|
crypto.SSECAlgorithm: "AES256",
|
||||||
|
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||||
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
|
},
|
||||||
|
metadata: map[string]string{
|
||||||
|
crypto.SSESealAlgorithm: "HMAC-SHA3",
|
||||||
|
crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||||
|
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||||
},
|
},
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
bucket: "bucket",
|
||||||
|
object: "object",
|
||||||
header: map[string]string{
|
header: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
||||||
ServerSideEncryptionIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
crypto.SSEIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||||
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||||
},
|
},
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
bucket: "bucket",
|
||||||
|
object: "object",
|
||||||
header: map[string]string{
|
header: map[string]string{
|
||||||
SSECustomerAlgorithm: "AES256",
|
crypto.SSECAlgorithm: "AES256",
|
||||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||||
SSECustomerKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256,
|
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
||||||
ServerSideEncryptionIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=",
|
crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=",
|
||||||
ServerSideEncryptionSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=",
|
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=",
|
||||||
|
},
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bucket: "bucket",
|
||||||
|
object: "object-2",
|
||||||
|
header: map[string]string{
|
||||||
|
crypto.SSECAlgorithm: "AES256",
|
||||||
|
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||||
|
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||||
|
},
|
||||||
|
metadata: map[string]string{
|
||||||
|
crypto.SSESealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
||||||
|
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||||
|
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||||
},
|
},
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
@@ -389,26 +490,29 @@ var decryptRequestTests = []struct {
|
|||||||
func TestDecryptRequest(t *testing.T) {
|
func TestDecryptRequest(t *testing.T) {
|
||||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
||||||
globalIsSSL = true
|
globalIsSSL = true
|
||||||
for i, test := range decryptRequestTests {
|
for i, test := range decryptRequestTests[1:] {
|
||||||
client := bytes.NewBuffer(nil)
|
client := bytes.NewBuffer(nil)
|
||||||
req := &http.Request{Header: http.Header{}}
|
req := &http.Request{Header: http.Header{}}
|
||||||
for k, v := range test.header {
|
for k, v := range test.header {
|
||||||
req.Header.Set(k, v)
|
req.Header.Set(k, v)
|
||||||
}
|
}
|
||||||
_, err := DecryptRequest(client, req, test.metadata)
|
_, err := DecryptRequest(client, req, test.bucket, test.object, test.metadata)
|
||||||
if err != nil && !test.shouldFail {
|
if err != nil && !test.shouldFail {
|
||||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||||
}
|
}
|
||||||
if key, ok := test.metadata[SSECustomerKey]; ok {
|
if err == nil && test.shouldFail {
|
||||||
|
t.Fatalf("Test %d: should fail but passed", i)
|
||||||
|
}
|
||||||
|
if key, ok := test.metadata[crypto.SSECKey]; ok {
|
||||||
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
||||||
}
|
}
|
||||||
if kdf, ok := test.metadata[ServerSideEncryptionSealAlgorithm]; ok && !test.shouldFail {
|
if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; ok && !test.shouldFail {
|
||||||
t.Errorf("Test %d: ServerSideEncryptionKDF should not be part of metadata: %v", i, kdf)
|
t.Errorf("Test %d: ServerSideEncryptionKDF should not be part of metadata: %v", i, kdf)
|
||||||
}
|
}
|
||||||
if iv, ok := test.metadata[ServerSideEncryptionIV]; ok && !test.shouldFail {
|
if iv, ok := test.metadata[crypto.SSEIV]; ok && !test.shouldFail {
|
||||||
t.Errorf("Test %d: ServerSideEncryptionIV should not be part of metadata: %v", i, iv)
|
t.Errorf("Test %d: crypto.SSEIV should not be part of metadata: %v", i, iv)
|
||||||
}
|
}
|
||||||
if mac, ok := test.metadata[ServerSideEncryptionSealedKey]; ok && !test.shouldFail {
|
if mac, ok := test.metadata[crypto.SSECSealedKey]; ok && !test.shouldFail {
|
||||||
t.Errorf("Test %d: ServerSideEncryptionKeyMAC should not be part of metadata: %v", i, mac)
|
t.Errorf("Test %d: ServerSideEncryptionKeyMAC should not be part of metadata: %v", i, mac)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,28 +529,28 @@ var decryptObjectInfoTests = []struct {
|
|||||||
expErr: ErrNone,
|
expErr: ErrNone,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: ErrNone,
|
expErr: ErrNone,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 0, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 0, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: ErrNone,
|
expErr: ErrNone,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA=="}},
|
||||||
headers: http.Header{},
|
headers: http.Header{},
|
||||||
expErr: ErrSSEEncryptedObject,
|
expErr: ErrSSEEncryptedObject,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{}},
|
info: ObjectInfo{Size: 100, UserDefined: map[string]string{}},
|
||||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: ErrInvalidEncryptionParameters,
|
expErr: ErrInvalidEncryptionParameters,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 31, UserDefined: map[string]string{ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 31, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
||||||
headers: http.Header{SSECustomerAlgorithm: []string{SSECustomerAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: ErrObjectTampered,
|
expErr: ErrObjectTampered,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -455,7 +559,7 @@ func TestDecryptObjectInfo(t *testing.T) {
|
|||||||
for i, test := range decryptObjectInfoTests {
|
for i, test := range decryptObjectInfoTests {
|
||||||
if err, encrypted := DecryptObjectInfo(&test.info, test.headers); err != test.expErr {
|
if err, encrypted := DecryptObjectInfo(&test.info, test.headers); err != test.expErr {
|
||||||
t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr)
|
t.Errorf("Test %d: Decryption returned wrong error code: got %d , want %d", i, err, test.expErr)
|
||||||
} else if enc := test.info.IsEncrypted(); encrypted && enc != encrypted {
|
} else if enc := crypto.IsEncrypted(test.info.UserDefined); encrypted && enc != encrypted {
|
||||||
t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i)
|
t.Errorf("Test %d: Decryption thinks object is encrypted but it is not", i)
|
||||||
} else if !encrypted && enc != encrypted {
|
} else if !encrypted && enc != encrypted {
|
||||||
t.Errorf("Test %d: Decryption thinks object is not encrypted but it is", i)
|
t.Errorf("Test %d: Decryption thinks object is not encrypted but it is", i)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/minio/minio-go/pkg/set"
|
"github.com/minio/minio-go/pkg/set"
|
||||||
@@ -69,10 +71,26 @@ func getSetIndexes(args []string, totalSizes []uint64) (setIndexes [][]uint64, e
|
|||||||
return nil, errInvalidArgument
|
return nil, errInvalidArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
|
||||||
|
isValidSetSize := func(count uint64) bool {
|
||||||
|
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1] && count%2 == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var customSetDriveCount uint64
|
||||||
|
if v := os.Getenv("MINIO_ERASURE_SET_DRIVE_COUNT"); v != "" {
|
||||||
|
customSetDriveCount, err = strconv.ParseUint(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, uiErrInvalidErasureSetSize(err)
|
||||||
|
}
|
||||||
|
if !isValidSetSize(customSetDriveCount) {
|
||||||
|
return nil, uiErrInvalidErasureSetSize(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setIndexes = make([][]uint64, len(totalSizes))
|
setIndexes = make([][]uint64, len(totalSizes))
|
||||||
for _, totalSize := range totalSizes {
|
for _, totalSize := range totalSizes {
|
||||||
// Check if totalSize has minimum range upto setSize
|
// Check if totalSize has minimum range upto setSize
|
||||||
if totalSize < setSizes[0] {
|
if totalSize < setSizes[0] || totalSize < customSetDriveCount {
|
||||||
return nil, uiErrInvalidNumberOfErasureEndpoints(nil)
|
return nil, uiErrInvalidNumberOfErasureEndpoints(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,9 +113,24 @@ func getSetIndexes(args []string, totalSizes []uint64) (setIndexes [][]uint64, e
|
|||||||
setSize = commonSize
|
setSize = commonSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
|
possibleSetCounts := func(setSize uint64) (ss []uint64) {
|
||||||
isValidSetSize := func(count uint64) bool {
|
for _, s := range setSizes {
|
||||||
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1] && count%2 == 0)
|
if setSize%s == 0 {
|
||||||
|
ss = append(ss, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
if customSetDriveCount > 0 {
|
||||||
|
msg := fmt.Sprintf("Invalid set drive count, leads to non-uniform distribution for the given number of disks. Possible values for custom set count are %d", possibleSetCounts(setSize))
|
||||||
|
if customSetDriveCount > setSize {
|
||||||
|
return nil, uiErrInvalidErasureSetSize(nil).Msg(msg)
|
||||||
|
}
|
||||||
|
if setSize%customSetDriveCount != 0 {
|
||||||
|
return nil, uiErrInvalidErasureSetSize(nil).Msg(msg)
|
||||||
|
}
|
||||||
|
setSize = customSetDriveCount
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether setSize is with the supported range.
|
// Check whether setSize is with the supported range.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -84,6 +85,65 @@ func TestGetDivisibleSize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test tests calculating set indexes with ENV override for drive count.
|
||||||
|
func TestGetSetIndexesEnvOverride(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
args []string
|
||||||
|
totalSizes []uint64
|
||||||
|
indexes [][]uint64
|
||||||
|
envOverride string
|
||||||
|
success bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]string{"data{1...64}"},
|
||||||
|
[]uint64{64},
|
||||||
|
[][]uint64{{8, 8, 8, 8, 8, 8, 8, 8}},
|
||||||
|
"8",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"data{1...60}"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"8",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"data{1...64}"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"-1",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]string{"data{1...64}"},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"2",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
|
||||||
|
if err := os.Setenv("MINIO_ERASURE_SET_DRIVE_COUNT", testCase.envOverride); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes)
|
||||||
|
if err != nil && testCase.success {
|
||||||
|
t.Errorf("Expected success but failed instead %s", err)
|
||||||
|
}
|
||||||
|
if err == nil && !testCase.success {
|
||||||
|
t.Errorf("Expected failure but passed instead")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(testCase.indexes, gotIndexes) {
|
||||||
|
t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes)
|
||||||
|
}
|
||||||
|
os.Unsetenv("MINIO_ERASURE_SET_DRIVE_COUNT")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test tests calculating set indexes.
|
// Test tests calculating set indexes.
|
||||||
func TestGetSetIndexes(t *testing.T) {
|
func TestGetSetIndexes(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
|||||||
+74
-67
@@ -18,85 +18,92 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"hash"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateFile creates a new bitrot encoded file spread over all available disks. CreateFile will create
|
// Writes in parallel to bitrotWriters
|
||||||
// the file at the given volume and path. It will read from src until an io.EOF occurs. The given algorithm will
|
type parallelWriter struct {
|
||||||
// be used to protect the erasure encoded file.
|
writers []*bitrotWriter
|
||||||
func (s *ErasureStorage) CreateFile(ctx context.Context, src io.Reader, volume, path string, buffer []byte, algorithm BitrotAlgorithm, writeQuorum int) (f ErasureFileInfo, err error) {
|
writeQuorum int
|
||||||
if !algorithm.Available() {
|
errs []error
|
||||||
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
|
}
|
||||||
return f, errBitrotHashAlgoInvalid
|
|
||||||
}
|
|
||||||
f.Checksums = make([][]byte, len(s.disks))
|
|
||||||
hashers := make([]hash.Hash, len(s.disks))
|
|
||||||
for i := range hashers {
|
|
||||||
hashers[i] = algorithm.New()
|
|
||||||
}
|
|
||||||
errChans, errs := make([]chan error, len(s.disks)), make([]error, len(s.disks))
|
|
||||||
for i := range errChans {
|
|
||||||
errChans[i] = make(chan error, 1) // create buffered channel to let finished go-routines die early
|
|
||||||
}
|
|
||||||
|
|
||||||
var blocks [][]byte
|
// Append appends data to bitrotWriters in parallel.
|
||||||
var n = len(buffer)
|
func (p *parallelWriter) Append(ctx context.Context, blocks [][]byte) error {
|
||||||
for n == len(buffer) {
|
var wg sync.WaitGroup
|
||||||
n, err = io.ReadFull(src, buffer)
|
|
||||||
if n == 0 && err == io.EOF {
|
|
||||||
if f.Size != 0 { // don't write empty block if we have written to the disks
|
|
||||||
break
|
|
||||||
}
|
|
||||||
blocks = make([][]byte, len(s.disks)) // write empty block
|
|
||||||
} else if err == nil || (n > 0 && err == io.ErrUnexpectedEOF) {
|
|
||||||
blocks, err = s.ErasureEncode(ctx, buffer[:n])
|
|
||||||
if err != nil {
|
|
||||||
return f, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return f, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range errChans { // span workers
|
for i := range p.writers {
|
||||||
go erasureAppendFile(ctx, s.disks[i], volume, path, hashers[i], blocks[i], errChans[i])
|
if p.writers[i] == nil {
|
||||||
}
|
p.errs[i] = errDiskNotFound
|
||||||
for i := range errChans { // wait until all workers are finished
|
|
||||||
errs[i] = <-errChans[i]
|
|
||||||
}
|
|
||||||
if err = reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum); err != nil {
|
|
||||||
return f, err
|
|
||||||
}
|
|
||||||
s.disks = evalDisks(s.disks, errs)
|
|
||||||
f.Size += int64(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Algorithm = algorithm
|
|
||||||
for i, disk := range s.disks {
|
|
||||||
if disk == OfflineDisk {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
f.Checksums[i] = hashers[i].Sum(nil)
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
p.errs[i] = p.writers[i].Append(blocks[i])
|
||||||
|
if p.errs[i] != nil {
|
||||||
|
p.writers[i] = nil
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
}
|
}
|
||||||
return f, nil
|
wg.Wait()
|
||||||
|
|
||||||
|
// If nilCount >= p.writeQuorum, we return nil. This is because HealFile() uses
|
||||||
|
// CreateFile with p.writeQuorum=1 to accommodate healing of single disk.
|
||||||
|
// i.e if we do no return here in such a case, reduceWriteQuorumErrs() would
|
||||||
|
// return a quorum error to HealFile().
|
||||||
|
nilCount := 0
|
||||||
|
for _, err := range p.errs {
|
||||||
|
if err == nil {
|
||||||
|
nilCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nilCount >= p.writeQuorum {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return reduceWriteQuorumErrs(ctx, p.errs, objectOpIgnoredErrs, p.writeQuorum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// erasureAppendFile appends the content of buf to the file on the given disk and updates computes
|
// CreateFile reads from the reader, erasure-encodes the data and writes to the writers.
|
||||||
// the hash of the written data. It sends the write error (or nil) over the error channel.
|
func (s *ErasureStorage) CreateFile(ctx context.Context, src io.Reader, writers []*bitrotWriter, buf []byte, quorum int) (total int64, err error) {
|
||||||
func erasureAppendFile(ctx context.Context, disk StorageAPI, volume, path string, hash hash.Hash, buf []byte, errChan chan<- error) {
|
writer := ¶llelWriter{
|
||||||
if disk == OfflineDisk {
|
writers: writers,
|
||||||
logger.LogIf(ctx, errDiskNotFound)
|
writeQuorum: quorum,
|
||||||
errChan <- errDiskNotFound
|
errs: make([]error, len(writers)),
|
||||||
return
|
|
||||||
}
|
}
|
||||||
err := disk.AppendFile(volume, path, buf)
|
|
||||||
if err != nil {
|
for {
|
||||||
errChan <- err
|
var blocks [][]byte
|
||||||
return
|
n, err := io.ReadFull(src, buf)
|
||||||
|
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
eof := err == io.EOF || err == io.ErrUnexpectedEOF
|
||||||
|
if n == 0 && total != 0 {
|
||||||
|
// Reached EOF, nothing more to be done.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// We take care of the situation where if n == 0 and total == 0 by creating empty data and parity files.
|
||||||
|
blocks, err = s.ErasureEncode(ctx, buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writer.Append(ctx, blocks); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
total += int64(n)
|
||||||
|
if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
hash.Write(buf)
|
return total, nil
|
||||||
errChan <- err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,15 +54,14 @@ var erasureCreateFileTests = []struct {
|
|||||||
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 9
|
{dataBlocks: 7, onDisks: 14, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 9
|
||||||
{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 10
|
{dataBlocks: 8, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 10
|
||||||
{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
|
{dataBlocks: 5, onDisks: 10, offDisks: 3, blocksize: int64(oneMiByte), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 11
|
||||||
{dataBlocks: 6, onDisks: 12, offDisks: 5, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 102, algorithm: 0, shouldFail: true, shouldFailQuorum: false}, // 12
|
{dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 12
|
||||||
{dataBlocks: 3, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
|
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 13
|
||||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(oneMiByte / 2), data: oneMiByte, offset: oneMiByte/2 + 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 14
|
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 14
|
||||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(oneMiByte - 1), data: oneMiByte, offset: oneMiByte - 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 15
|
{dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 15
|
||||||
{dataBlocks: 8, onDisks: 12, offDisks: 2, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16
|
{dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 16
|
||||||
{dataBlocks: 8, onDisks: 10, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
|
{dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 17
|
||||||
{dataBlocks: 10, onDisks: 14, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 17, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18
|
{dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 18
|
||||||
{dataBlocks: 2, onDisks: 6, offDisks: 2, blocksize: int64(oneMiByte), data: oneMiByte, offset: oneMiByte / 2, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 19
|
{dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 19
|
||||||
{dataBlocks: 10, onDisks: 16, offDisks: 8, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 20
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErasureCreateFile(t *testing.T) {
|
func TestErasureCreateFile(t *testing.T) {
|
||||||
@@ -71,7 +70,8 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||||
}
|
}
|
||||||
storage, err := NewErasureStorage(context.Background(), setup.disks, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
disks := setup.disks
|
||||||
|
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||||
@@ -83,25 +83,43 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
||||||
}
|
}
|
||||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), "testbucket", "object", buffer, test.algorithm, test.dataBlocks+1)
|
writers := make([]*bitrotWriter, len(disks))
|
||||||
|
for i, disk := range disks {
|
||||||
|
if disk == OfflineDisk {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writers[i] = newBitrotWriter(disk, "testbucket", "object", test.algorithm)
|
||||||
|
}
|
||||||
|
n, err := storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, storage.dataBlocks+1)
|
||||||
if err != nil && !test.shouldFail {
|
if err != nil && !test.shouldFail {
|
||||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
if err == nil && test.shouldFail {
|
if err == nil && test.shouldFail {
|
||||||
t.Errorf("Test %d: should fail but it passed", i)
|
t.Errorf("Test %d: should fail but it passed", i)
|
||||||
}
|
}
|
||||||
|
for i, w := range writers {
|
||||||
if err == nil {
|
if w == nil {
|
||||||
if length := int64(len(data[test.offset:])); file.Size != length {
|
disks[i] = OfflineDisk
|
||||||
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, file.Size, length)
|
|
||||||
}
|
}
|
||||||
for j := range storage.disks[:test.offDisks] {
|
}
|
||||||
storage.disks[j] = badDisk{nil}
|
if err == nil {
|
||||||
|
if length := int64(len(data[test.offset:])); n != length {
|
||||||
|
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
|
||||||
|
}
|
||||||
|
writers := make([]*bitrotWriter, len(disks))
|
||||||
|
for i, disk := range disks {
|
||||||
|
if disk == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writers[i] = newBitrotWriter(disk, "testbucket", "object2", test.algorithm)
|
||||||
|
}
|
||||||
|
for j := range disks[:test.offDisks] {
|
||||||
|
writers[j].disk = badDisk{nil}
|
||||||
}
|
}
|
||||||
if test.offDisks > 0 {
|
if test.offDisks > 0 {
|
||||||
storage.disks[0] = OfflineDisk
|
writers[0] = nil
|
||||||
}
|
}
|
||||||
file, err = storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), "testbucket", "object2", buffer, test.algorithm, test.dataBlocks+1)
|
n, err = storage.CreateFile(context.Background(), bytes.NewReader(data[test.offset:]), writers, buffer, storage.dataBlocks+1)
|
||||||
if err != nil && !test.shouldFailQuorum {
|
if err != nil && !test.shouldFailQuorum {
|
||||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -109,8 +127,8 @@ func TestErasureCreateFile(t *testing.T) {
|
|||||||
t.Errorf("Test %d: should fail but it passed", i)
|
t.Errorf("Test %d: should fail but it passed", i)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if length := int64(len(data[test.offset:])); file.Size != length {
|
if length := int64(len(data[test.offset:])); n != length {
|
||||||
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, file.Size, length)
|
t.Errorf("Test %d: invalid number of bytes written: got: #%d want #%d", i, n, length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,25 +144,33 @@ func benchmarkErasureWrite(data, parity, dataDown, parityDown int, size int64, b
|
|||||||
b.Fatalf("failed to create test setup: %v", err)
|
b.Fatalf("failed to create test setup: %v", err)
|
||||||
}
|
}
|
||||||
defer setup.Remove()
|
defer setup.Remove()
|
||||||
storage, err := NewErasureStorage(context.Background(), setup.disks, data, parity, blockSizeV1)
|
storage, err := NewErasureStorage(context.Background(), data, parity, blockSizeV1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||||
}
|
}
|
||||||
|
disks := setup.disks
|
||||||
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
||||||
content := make([]byte, size)
|
content := make([]byte, size)
|
||||||
|
|
||||||
for i := 0; i < dataDown; i++ {
|
for i := 0; i < dataDown; i++ {
|
||||||
storage.disks[i] = OfflineDisk
|
disks[i] = OfflineDisk
|
||||||
}
|
}
|
||||||
for i := data; i < data+parityDown; i++ {
|
for i := data; i < data+parityDown; i++ {
|
||||||
storage.disks[i] = OfflineDisk
|
disks[i] = OfflineDisk
|
||||||
}
|
}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.SetBytes(size)
|
b.SetBytes(size)
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_, err := storage.CreateFile(context.Background(), bytes.NewReader(content), "testbucket", "object", buffer, DefaultBitrotAlgorithm, data+1)
|
writers := make([]*bitrotWriter, len(disks))
|
||||||
|
for i, disk := range disks {
|
||||||
|
if disk == OfflineDisk {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||||
|
}
|
||||||
|
_, err := storage.CreateFile(context.Background(), bytes.NewReader(content), writers, buffer, storage.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-159
@@ -18,171 +18,31 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"io"
|
||||||
"hash"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HealFile tries to reconstruct an erasure-coded file spread over all
|
// HealFile heals the shard files on non-nil writers. Note that the quorum passed is 1
|
||||||
// available disks. HealFile will read the valid parts of the file,
|
// as healing should continue even if it has been successful healing only one shard file.
|
||||||
// reconstruct the missing data and write the reconstructed parts back
|
func (s ErasureStorage) HealFile(ctx context.Context, readers []*bitrotReader, writers []*bitrotWriter, size int64) error {
|
||||||
// to `staleDisks` at the destination `dstVol/dstPath/`. Parts are
|
r, w := io.Pipe()
|
||||||
// verified against the given BitrotAlgorithm and checksums.
|
go func() {
|
||||||
//
|
if err := s.ReadFile(ctx, w, readers, 0, size, size); err != nil {
|
||||||
// `staleDisks` is a slice of disks where each non-nil entry has stale
|
w.CloseWithError(err)
|
||||||
// or no data, and so will be healed.
|
return
|
||||||
//
|
|
||||||
// It is required that `s.disks` have a (read-quorum) majority of
|
|
||||||
// disks with valid data for healing to work.
|
|
||||||
//
|
|
||||||
// In addition, `staleDisks` and `s.disks` must have the same ordering
|
|
||||||
// of disks w.r.t. erasure coding of the object.
|
|
||||||
//
|
|
||||||
// Errors when writing to `staleDisks` are not propagated as long as
|
|
||||||
// writes succeed for at least one disk. This allows partial healing
|
|
||||||
// despite stale disks being faulty.
|
|
||||||
//
|
|
||||||
// It returns bitrot checksums for the non-nil staleDisks on which
|
|
||||||
// healing succeeded.
|
|
||||||
func (s ErasureStorage) HealFile(ctx context.Context, staleDisks []StorageAPI, volume, path string, blocksize int64,
|
|
||||||
dstVol, dstPath string, size int64, alg BitrotAlgorithm, checksums [][]byte) (
|
|
||||||
f ErasureFileInfo, err error) {
|
|
||||||
|
|
||||||
if !alg.Available() {
|
|
||||||
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
|
|
||||||
return f, errBitrotHashAlgoInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialization
|
|
||||||
f.Checksums = make([][]byte, len(s.disks))
|
|
||||||
hashers := make([]hash.Hash, len(s.disks))
|
|
||||||
verifiers := make([]*BitrotVerifier, len(s.disks))
|
|
||||||
for i, disk := range s.disks {
|
|
||||||
switch {
|
|
||||||
case staleDisks[i] != nil:
|
|
||||||
hashers[i] = alg.New()
|
|
||||||
case disk == nil:
|
|
||||||
// disregard unavailable disk
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
verifiers[i] = NewBitrotVerifier(alg, checksums[i])
|
|
||||||
}
|
}
|
||||||
}
|
w.Close()
|
||||||
writeErrors := make([]error, len(s.disks))
|
}()
|
||||||
|
buf := make([]byte, s.blockSize)
|
||||||
// Read part file data on each disk
|
// quorum is 1 because CreateFile should continue writing as long as we are writing to even 1 disk.
|
||||||
chunksize := ceilFrac(blocksize, int64(s.dataBlocks))
|
n, err := s.CreateFile(ctx, r, writers, buf, 1)
|
||||||
numBlocks := ceilFrac(size, blocksize)
|
|
||||||
|
|
||||||
readLen := chunksize * (numBlocks - 1)
|
|
||||||
|
|
||||||
lastChunkSize := chunksize
|
|
||||||
hasSmallerLastBlock := size%blocksize != 0
|
|
||||||
if hasSmallerLastBlock {
|
|
||||||
lastBlockLen := size % blocksize
|
|
||||||
lastChunkSize = ceilFrac(lastBlockLen, int64(s.dataBlocks))
|
|
||||||
}
|
|
||||||
readLen += lastChunkSize
|
|
||||||
var buffers [][]byte
|
|
||||||
buffers, _, err = s.readConcurrent(ctx, volume, path, 0, readLen, verifiers)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return f, err
|
return err
|
||||||
}
|
}
|
||||||
|
if n != size {
|
||||||
// Scan part files on disk, block-by-block reconstruct it and
|
logger.LogIf(ctx, errLessData)
|
||||||
// write to stale disks.
|
return errLessData
|
||||||
blocks := make([][]byte, len(s.disks))
|
|
||||||
|
|
||||||
if numBlocks > 1 {
|
|
||||||
// Allocate once for all the equal length blocks. The
|
|
||||||
// last block may have a different length - allocation
|
|
||||||
// for this happens inside the for loop below.
|
|
||||||
for i := range blocks {
|
|
||||||
if len(buffers[i]) == 0 {
|
|
||||||
blocks[i] = make([]byte, chunksize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
var buffOffset int64
|
|
||||||
for blockNumber := int64(0); blockNumber < numBlocks; blockNumber++ {
|
|
||||||
if blockNumber == numBlocks-1 && lastChunkSize != chunksize {
|
|
||||||
for i := range blocks {
|
|
||||||
if len(buffers[i]) == 0 {
|
|
||||||
blocks[i] = make([]byte, lastChunkSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range blocks {
|
|
||||||
if len(buffers[i]) == 0 {
|
|
||||||
blocks[i] = blocks[i][0:0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
csize := chunksize
|
|
||||||
if blockNumber == numBlocks-1 {
|
|
||||||
csize = lastChunkSize
|
|
||||||
}
|
|
||||||
for i := range blocks {
|
|
||||||
if len(buffers[i]) != 0 {
|
|
||||||
blocks[i] = buffers[i][buffOffset : buffOffset+csize]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buffOffset += csize
|
|
||||||
|
|
||||||
if err = s.ErasureDecodeDataAndParityBlocks(ctx, blocks); err != nil {
|
|
||||||
return f, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// write computed shards as chunks on file in each
|
|
||||||
// stale disk
|
|
||||||
writeSucceeded := false
|
|
||||||
for i, disk := range staleDisks {
|
|
||||||
// skip nil disk or disk that had error on
|
|
||||||
// previous write
|
|
||||||
if disk == nil || writeErrors[i] != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
writeErrors[i] = disk.AppendFile(dstVol, dstPath, blocks[i])
|
|
||||||
if writeErrors[i] == nil {
|
|
||||||
hashers[i].Write(blocks[i])
|
|
||||||
writeSucceeded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all disks had write errors we quit.
|
|
||||||
if !writeSucceeded {
|
|
||||||
// build error from all write errors
|
|
||||||
err := joinWriteErrors(writeErrors)
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return f, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy computed file hashes into output variable
|
|
||||||
f.Size = size
|
|
||||||
f.Algorithm = alg
|
|
||||||
for i, disk := range staleDisks {
|
|
||||||
if disk == nil || writeErrors[i] != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.Checksums[i] = hashers[i].Sum(nil)
|
|
||||||
}
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinWriteErrors(errs []error) error {
|
|
||||||
msgs := []string{}
|
|
||||||
for i, err := range errs {
|
|
||||||
if err == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
msgs = append(msgs, fmt.Sprintf("disk %d: %v", i+1, err))
|
|
||||||
}
|
|
||||||
return fmt.Errorf("all stale disks had write errors during healing: %s",
|
|
||||||
strings.Join(msgs, ", "))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,11 +55,9 @@ var erasureHealFileTests = []struct {
|
|||||||
{dataBlocks: 7, disks: 14, offDisks: 6, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 14
|
{dataBlocks: 7, disks: 14, offDisks: 6, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 14
|
||||||
{dataBlocks: 8, disks: 16, offDisks: 4, badDisks: 5, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 15
|
{dataBlocks: 8, disks: 16, offDisks: 4, badDisks: 5, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: true}, // 15
|
||||||
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 16
|
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 16
|
||||||
{dataBlocks: 2, disks: 4, offDisks: 0, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: 0, shouldFail: true}, // 17
|
{dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 17
|
||||||
{dataBlocks: 12, disks: 16, offDisks: 2, badDisks: 1, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false}, // 18
|
{dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 18
|
||||||
{dataBlocks: 6, disks: 8, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: BLAKE2b512, shouldFail: false}, // 19
|
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 19
|
||||||
{dataBlocks: 7, disks: 10, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte, algorithm: 0, shouldFail: true}, // 20
|
|
||||||
{dataBlocks: 2, disks: 4, offDisks: 1, badDisks: 0, badStaleDisks: 0, blocksize: int64(blockSizeV1), size: oneMiByte * 64, algorithm: SHA256, shouldFail: false}, // 21
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErasureHealFile(t *testing.T) {
|
func TestErasureHealFile(t *testing.T) {
|
||||||
@@ -75,7 +72,8 @@ func TestErasureHealFile(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to setup XL environment: %v", i, err)
|
t.Fatalf("Test %d: failed to setup XL environment: %v", i, err)
|
||||||
}
|
}
|
||||||
storage, err := NewErasureStorage(context.Background(), setup.disks, test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
disks := setup.disks
|
||||||
|
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.disks-test.dataBlocks, test.blocksize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||||
@@ -85,36 +83,50 @@ func TestErasureHealFile(t *testing.T) {
|
|||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||||
}
|
}
|
||||||
algorithm := test.algorithm
|
|
||||||
if !algorithm.Available() {
|
|
||||||
algorithm = DefaultBitrotAlgorithm
|
|
||||||
}
|
|
||||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data), "testbucket", "testobject", buffer, algorithm, test.dataBlocks+1)
|
writers := make([]*bitrotWriter, len(disks))
|
||||||
|
for i, disk := range disks {
|
||||||
|
writers[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
|
||||||
|
}
|
||||||
|
_, err = storage.CreateFile(context.Background(), bytes.NewReader(data), writers, buffer, storage.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
t.Fatalf("Test %d: failed to create random test data: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readers := make([]*bitrotReader, len(disks))
|
||||||
|
for i, disk := range disks {
|
||||||
|
shardFilesize := getErasureShardFileSize(test.blocksize, test.size, storage.dataBlocks)
|
||||||
|
readers[i] = newBitrotReader(disk, "testbucket", "testobject", test.algorithm, shardFilesize, writers[i].Sum())
|
||||||
|
}
|
||||||
|
|
||||||
// setup stale disks for the test case
|
// setup stale disks for the test case
|
||||||
staleDisks := make([]StorageAPI, len(storage.disks))
|
staleDisks := make([]StorageAPI, len(disks))
|
||||||
copy(staleDisks, storage.disks)
|
copy(staleDisks, disks)
|
||||||
for j := 0; j < len(storage.disks); j++ {
|
for j := 0; j < len(staleDisks); j++ {
|
||||||
if j < test.offDisks {
|
if j < test.offDisks {
|
||||||
storage.disks[j] = OfflineDisk
|
readers[j] = nil
|
||||||
} else {
|
} else {
|
||||||
staleDisks[j] = nil
|
staleDisks[j] = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for j := 0; j < test.badDisks; j++ {
|
for j := 0; j < test.badDisks; j++ {
|
||||||
storage.disks[test.offDisks+j] = badDisk{nil}
|
readers[test.offDisks+j].disk = badDisk{nil}
|
||||||
}
|
}
|
||||||
for j := 0; j < test.badStaleDisks; j++ {
|
for j := 0; j < test.badStaleDisks; j++ {
|
||||||
staleDisks[j] = badDisk{nil}
|
staleDisks[j] = badDisk{nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
staleWriters := make([]*bitrotWriter, len(staleDisks))
|
||||||
|
for i, disk := range staleDisks {
|
||||||
|
if disk == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
staleWriters[i] = newBitrotWriter(disk, "testbucket", "testobject", test.algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
// test case setup is complete - now call Healfile()
|
// test case setup is complete - now call Healfile()
|
||||||
info, err := storage.HealFile(context.Background(), staleDisks, "testbucket", "testobject", test.blocksize, "testbucket", "healedobject", test.size, test.algorithm, file.Checksums)
|
err = storage.HealFile(context.Background(), readers, staleWriters, test.size)
|
||||||
if err != nil && !test.shouldFail {
|
if err != nil && !test.shouldFail {
|
||||||
t.Errorf("Test %d: should pass but it failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but it failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -122,19 +134,13 @@ func TestErasureHealFile(t *testing.T) {
|
|||||||
t.Errorf("Test %d: should fail but it passed", i)
|
t.Errorf("Test %d: should fail but it passed", i)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if info.Size != test.size {
|
|
||||||
t.Errorf("Test %d: healed wrong number of bytes: got: #%d want: #%d", i, info.Size, test.size)
|
|
||||||
}
|
|
||||||
if info.Algorithm != test.algorithm {
|
|
||||||
t.Errorf("Test %d: healed with wrong algorithm: got: %v want: %v", i, info.Algorithm, test.algorithm)
|
|
||||||
}
|
|
||||||
// Verify that checksums of staleDisks
|
// Verify that checksums of staleDisks
|
||||||
// match expected values
|
// match expected values
|
||||||
for i, disk := range staleDisks {
|
for i := range staleWriters {
|
||||||
if disk == nil || info.Checksums[i] == nil {
|
if staleWriters[i] == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(info.Checksums[i], file.Checksums[i]) {
|
if !bytes.Equal(staleWriters[i].Sum(), writers[i].Sum()) {
|
||||||
t.Errorf("Test %d: heal returned different bitrot checksums", i)
|
t.Errorf("Test %d: heal returned different bitrot checksums", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-181
@@ -23,214 +23,166 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type errIdx struct {
|
// Reads in parallel from bitrotReaders.
|
||||||
idx int
|
type parallelReader struct {
|
||||||
err error
|
readers []*bitrotReader
|
||||||
|
dataBlocks int
|
||||||
|
offset int64
|
||||||
|
shardSize int64
|
||||||
|
shardFileSize int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s ErasureStorage) readConcurrent(ctx context.Context, volume, path string, offset, length int64,
|
// newParallelReader returns parallelReader.
|
||||||
verifiers []*BitrotVerifier) (buffers [][]byte, needsReconstruction bool,
|
func newParallelReader(readers []*bitrotReader, dataBlocks int, offset int64, fileSize int64, blocksize int64) *parallelReader {
|
||||||
err error) {
|
shardSize := ceilFrac(blocksize, int64(dataBlocks))
|
||||||
|
shardFileSize := getErasureShardFileSize(blocksize, fileSize, dataBlocks)
|
||||||
|
return ¶llelReader{
|
||||||
|
readers,
|
||||||
|
dataBlocks,
|
||||||
|
(offset / blocksize) * shardSize,
|
||||||
|
shardSize,
|
||||||
|
shardFileSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
errChan := make(chan errIdx)
|
// Returns if buf can be erasure decoded.
|
||||||
stageBuffers := make([][]byte, len(s.disks))
|
func (p *parallelReader) canDecode(buf [][]byte) bool {
|
||||||
buffers = make([][]byte, len(s.disks))
|
bufCount := 0
|
||||||
|
for _, b := range buf {
|
||||||
readDisk := func(i int) {
|
if b != nil {
|
||||||
stageBuffers[i] = make([]byte, length)
|
bufCount++
|
||||||
disk := s.disks[i]
|
|
||||||
if disk == OfflineDisk {
|
|
||||||
logger.LogIf(ctx, errDiskNotFound)
|
|
||||||
errChan <- errIdx{i, errDiskNotFound}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
_, rerr := disk.ReadFile(volume, path, offset, stageBuffers[i], verifiers[i])
|
}
|
||||||
errChan <- errIdx{i, rerr}
|
return bufCount >= p.dataBlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads from bitrotReaders in parallel. Returns p.dataBlocks number of bufs.
|
||||||
|
func (p *parallelReader) Read() ([][]byte, error) {
|
||||||
|
type errIdx struct {
|
||||||
|
idx int
|
||||||
|
buf []byte
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
var finishedCount, successCount, launchIndex int
|
errCh := make(chan errIdx)
|
||||||
|
currReaderIndex := 0
|
||||||
|
newBuf := make([][]byte, len(p.readers))
|
||||||
|
|
||||||
for ; launchIndex < s.dataBlocks; launchIndex++ {
|
if p.offset+p.shardSize > p.shardFileSize {
|
||||||
go readDisk(launchIndex)
|
p.shardSize = p.shardFileSize - p.offset
|
||||||
}
|
}
|
||||||
for finishedCount < launchIndex {
|
|
||||||
select {
|
|
||||||
case errVal := <-errChan:
|
|
||||||
finishedCount++
|
|
||||||
if errVal.err != nil {
|
|
||||||
// TODO: meaningfully log the disk read error
|
|
||||||
|
|
||||||
// A disk failed to return data, so we
|
read := func(currReaderIndex int) {
|
||||||
// request an additional disk if possible
|
b, err := p.readers[currReaderIndex].ReadChunk(p.offset, p.shardSize)
|
||||||
if launchIndex < s.dataBlocks+s.parityBlocks {
|
errCh <- errIdx{currReaderIndex, b, err}
|
||||||
needsReconstruction = true
|
}
|
||||||
// requiredBlocks++
|
|
||||||
go readDisk(launchIndex)
|
readerCount := 0
|
||||||
launchIndex++
|
for _, r := range p.readers {
|
||||||
}
|
if r != nil {
|
||||||
} else {
|
readerCount++
|
||||||
successCount++
|
}
|
||||||
buffers[errVal.idx] = stageBuffers[errVal.idx]
|
}
|
||||||
stageBuffers[errVal.idx] = nil
|
if readerCount < p.dataBlocks {
|
||||||
|
return nil, errXLReadQuorum
|
||||||
|
}
|
||||||
|
|
||||||
|
readerCount = 0
|
||||||
|
for i, r := range p.readers {
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go read(i)
|
||||||
|
readerCount++
|
||||||
|
if readerCount == p.dataBlocks {
|
||||||
|
currReaderIndex = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for errVal := range errCh {
|
||||||
|
if errVal.err == nil {
|
||||||
|
newBuf[errVal.idx] = errVal.buf
|
||||||
|
if p.canDecode(newBuf) {
|
||||||
|
p.offset += int64(p.shardSize)
|
||||||
|
return newBuf, nil
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
p.readers[errVal.idx] = nil
|
||||||
|
for currReaderIndex < len(p.readers) {
|
||||||
|
if p.readers[currReaderIndex] != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
currReaderIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
if currReaderIndex == len(p.readers) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
go read(currReaderIndex)
|
||||||
|
currReaderIndex++
|
||||||
}
|
}
|
||||||
if successCount != s.dataBlocks {
|
|
||||||
// Not enough disks returns data.
|
return nil, errXLReadQuorum
|
||||||
err = errXLReadQuorum
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFile reads as much data as requested from the file under the
|
// ReadFile reads from readers, reconstructs data if needed and writes the data to the writer.
|
||||||
// given volume and path and writes the data to the provided writer.
|
func (s ErasureStorage) ReadFile(ctx context.Context, writer io.Writer, readers []*bitrotReader, offset, length, totalLength int64) error {
|
||||||
// The algorithm and the keys/checksums are used to verify the
|
|
||||||
// integrity of the given file. ReadFile will read data from the given
|
|
||||||
// offset up to the given length. If parts of the file are corrupted
|
|
||||||
// ReadFile tries to reconstruct the data.
|
|
||||||
func (s ErasureStorage) ReadFile(ctx context.Context, writer io.Writer, volume, path string, offset,
|
|
||||||
length, totalLength int64, checksums [][]byte, algorithm BitrotAlgorithm,
|
|
||||||
blocksize int64) (f ErasureFileInfo, err error) {
|
|
||||||
|
|
||||||
if offset < 0 || length < 0 {
|
if offset < 0 || length < 0 {
|
||||||
logger.LogIf(ctx, errUnexpected)
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
return f, errUnexpected
|
return errInvalidArgument
|
||||||
}
|
}
|
||||||
if offset+length > totalLength {
|
if offset+length > totalLength {
|
||||||
logger.LogIf(ctx, errUnexpected)
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
return f, errUnexpected
|
return errInvalidArgument
|
||||||
}
|
}
|
||||||
if !algorithm.Available() {
|
|
||||||
logger.LogIf(ctx, errBitrotHashAlgoInvalid)
|
|
||||||
return f, errBitrotHashAlgoInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Checksums = make([][]byte, len(s.disks))
|
|
||||||
verifiers := make([]*BitrotVerifier, len(s.disks))
|
|
||||||
for i, disk := range s.disks {
|
|
||||||
if disk == OfflineDisk {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
verifiers[i] = NewBitrotVerifier(algorithm, checksums[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
chunksize := ceilFrac(blocksize, int64(s.dataBlocks))
|
|
||||||
|
|
||||||
// We read all whole-blocks of erasure coded data containing
|
|
||||||
// the requested data range.
|
|
||||||
//
|
|
||||||
// The start index of the erasure coded block containing the
|
|
||||||
// `offset` byte of data is:
|
|
||||||
partDataStartIndex := (offset / blocksize) * chunksize
|
|
||||||
// The start index of the erasure coded block containing the
|
|
||||||
// (last) byte of data at the index `offset + length - 1` is:
|
|
||||||
blockStartIndex := ((offset + length - 1) / blocksize) * chunksize
|
|
||||||
// However, we need the end index of the e.c. block containing
|
|
||||||
// the last byte - we need to check if that block is the last
|
|
||||||
// block in the part (in that case, it may be have a different
|
|
||||||
// chunk size)
|
|
||||||
isLastBlock := (totalLength-1)/blocksize == (offset+length-1)/blocksize
|
|
||||||
var partDataEndIndex int64
|
|
||||||
if isLastBlock {
|
|
||||||
lastBlockChunkSize := chunksize
|
|
||||||
if totalLength%blocksize != 0 {
|
|
||||||
lastBlockChunkSize = ceilFrac(totalLength%blocksize, int64(s.dataBlocks))
|
|
||||||
}
|
|
||||||
partDataEndIndex = blockStartIndex + lastBlockChunkSize - 1
|
|
||||||
} else {
|
|
||||||
partDataEndIndex = blockStartIndex + chunksize - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thus, the length of data to be read from the part file(s) is:
|
|
||||||
partDataLength := partDataEndIndex - partDataStartIndex + 1
|
|
||||||
// The calculation above does not apply when length == 0:
|
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
partDataLength = 0
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffers [][]byte
|
reader := newParallelReader(readers, s.dataBlocks, offset, totalLength, s.blockSize)
|
||||||
var needsReconstruction bool
|
|
||||||
buffers, needsReconstruction, err = s.readConcurrent(ctx, volume, path,
|
|
||||||
partDataStartIndex, partDataLength, verifiers)
|
|
||||||
if err != nil {
|
|
||||||
// Could not read enough disks.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
numChunks := ceilFrac(partDataLength, chunksize)
|
startBlock := offset / s.blockSize
|
||||||
blocks := make([][]byte, len(s.disks))
|
endBlock := (offset + length) / s.blockSize
|
||||||
|
|
||||||
if needsReconstruction && numChunks > 1 {
|
var bytesWritten int64
|
||||||
// Allocate once for all the equal length blocks. The
|
for block := startBlock; block <= endBlock; block++ {
|
||||||
// last block may have a different length - allocation
|
var blockOffset, blockLength int64
|
||||||
// for this happens inside the for loop below.
|
switch {
|
||||||
for i := range blocks {
|
case startBlock == endBlock:
|
||||||
if len(buffers[i]) == 0 {
|
blockOffset = offset % s.blockSize
|
||||||
blocks[i] = make([]byte, chunksize)
|
blockLength = length
|
||||||
}
|
case block == startBlock:
|
||||||
|
blockOffset = offset % s.blockSize
|
||||||
|
blockLength = s.blockSize - blockOffset
|
||||||
|
case block == endBlock:
|
||||||
|
blockOffset = 0
|
||||||
|
blockLength = (offset + length) % s.blockSize
|
||||||
|
default:
|
||||||
|
blockOffset = 0
|
||||||
|
blockLength = s.blockSize
|
||||||
}
|
}
|
||||||
}
|
if blockLength == 0 {
|
||||||
|
break
|
||||||
var buffOffset int64
|
|
||||||
for chunkNumber := int64(0); chunkNumber < numChunks; chunkNumber++ {
|
|
||||||
if chunkNumber == numChunks-1 && partDataLength%chunksize != 0 {
|
|
||||||
chunksize = partDataLength % chunksize
|
|
||||||
// We allocate again as the last chunk has a
|
|
||||||
// different size.
|
|
||||||
for i := range blocks {
|
|
||||||
if len(buffers[i]) == 0 {
|
|
||||||
blocks[i] = make([]byte, chunksize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
bufs, err := reader.Read()
|
||||||
for i := range blocks {
|
|
||||||
if len(buffers[i]) == 0 {
|
|
||||||
blocks[i] = blocks[i][0:0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range blocks {
|
|
||||||
if len(buffers[i]) != 0 {
|
|
||||||
blocks[i] = buffers[i][buffOffset : buffOffset+chunksize]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buffOffset += chunksize
|
|
||||||
|
|
||||||
if needsReconstruction {
|
|
||||||
if err = s.ErasureDecodeDataBlocks(blocks); err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return f, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var writeStart int64
|
|
||||||
if chunkNumber == 0 {
|
|
||||||
writeStart = offset % blocksize
|
|
||||||
}
|
|
||||||
|
|
||||||
writeLength := blocksize - writeStart
|
|
||||||
if chunkNumber == numChunks-1 {
|
|
||||||
lastBlockLength := (offset + length) % blocksize
|
|
||||||
if lastBlockLength != 0 {
|
|
||||||
writeLength = lastBlockLength - writeStart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n, err := writeDataBlocks(ctx, writer, blocks, s.dataBlocks, writeStart, writeLength)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return f, err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = s.ErasureDecodeDataBlocks(bufs); err != nil {
|
||||||
f.Size += n
|
logger.LogIf(ctx, err)
|
||||||
}
|
return err
|
||||||
|
|
||||||
f.Algorithm = algorithm
|
|
||||||
for i, disk := range s.disks {
|
|
||||||
if disk == OfflineDisk || buffers[i] == nil {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
f.Checksums[i] = verifiers[i].Sum(nil)
|
n, err := writeDataBlocks(ctx, writer, bufs, s.dataBlocks, blockOffset, blockLength)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bytesWritten += n
|
||||||
}
|
}
|
||||||
return f, nil
|
if bytesWritten != length {
|
||||||
|
logger.LogIf(ctx, errLessData)
|
||||||
|
return errLessData
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-47
@@ -19,11 +19,12 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
crand "crypto/rand"
|
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
crand "crypto/rand"
|
||||||
|
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,19 +67,18 @@ var erasureReadFileTests = []struct {
|
|||||||
{dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 23
|
{dataBlocks: 8, onDisks: 16, offDisks: 7, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 23
|
||||||
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 24
|
{dataBlocks: 2, onDisks: 4, offDisks: 1, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 24
|
||||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 25
|
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 25
|
||||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: oneMiByte, offset: 0, length: oneMiByte, algorithm: 0, shouldFail: true, shouldFailQuorum: false}, // 26
|
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1) + 1, offset: 0, length: int64(blockSizeV1) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 26
|
||||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1) + 1, offset: 0, length: int64(blockSizeV1) + 1, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27
|
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 27
|
||||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 28
|
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 28
|
||||||
{dataBlocks: 3, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29
|
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 29
|
||||||
{dataBlocks: 4, onDisks: 8, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30
|
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 512, length: int64(blockSizeV1) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 30
|
||||||
{dataBlocks: 6, onDisks: 12, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 512, length: int64(blockSizeV1) + 8*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31
|
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: int64(blockSizeV1), length: int64(blockSizeV1) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 31
|
||||||
{dataBlocks: 8, onDisks: 16, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: int64(blockSizeV1), length: int64(blockSizeV1) - 1, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 32
|
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 32
|
||||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: -1, length: 3, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33
|
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 33
|
||||||
{dataBlocks: 2, onDisks: 4, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(oneMiByte), offset: 1024, length: -1, algorithm: DefaultBitrotAlgorithm, shouldFail: true, shouldFailQuorum: false}, // 34
|
{dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1), offset: 0, length: int64(blockSizeV1), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 34
|
||||||
{dataBlocks: 4, onDisks: 6, offDisks: 0, blocksize: int64(blockSizeV1), data: int64(blockSizeV1), offset: 0, length: int64(blockSizeV1), algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35
|
{dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 35
|
||||||
{dataBlocks: 4, onDisks: 6, offDisks: 1, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 12, length: int64(blockSizeV1) + 17, algorithm: BLAKE2b512, shouldFail: false, shouldFailQuorum: false}, // 36
|
{dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 36
|
||||||
{dataBlocks: 4, onDisks: 6, offDisks: 3, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 1023, length: int64(blockSizeV1) + 1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: true}, // 37
|
{dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 37
|
||||||
{dataBlocks: 8, onDisks: 12, offDisks: 4, blocksize: int64(blockSizeV1), data: int64(2 * blockSizeV1), offset: 11, length: int64(blockSizeV1) + 2*1024, algorithm: DefaultBitrotAlgorithm, shouldFail: false, shouldFailQuorum: false}, // 38
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErasureReadFile(t *testing.T) {
|
func TestErasureReadFile(t *testing.T) {
|
||||||
@@ -87,29 +87,54 @@ func TestErasureReadFile(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
t.Fatalf("Test %d: failed to create test setup: %v", i, err)
|
||||||
}
|
}
|
||||||
storage, err := NewErasureStorage(context.Background(), setup.disks, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
t.Fatalf("Test %d: failed to create ErasureStorage: %v", i, err)
|
||||||
}
|
}
|
||||||
|
disks := setup.disks
|
||||||
data := make([]byte, test.data)
|
data := make([]byte, test.data)
|
||||||
if _, err = io.ReadFull(crand.Reader, data); err != nil {
|
if _, err = io.ReadFull(crand.Reader, data); err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
t.Fatalf("Test %d: failed to generate random test data: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeAlgorithm := test.algorithm
|
writeAlgorithm := test.algorithm
|
||||||
if !test.algorithm.Available() {
|
if !test.algorithm.Available() {
|
||||||
writeAlgorithm = DefaultBitrotAlgorithm
|
writeAlgorithm = DefaultBitrotAlgorithm
|
||||||
}
|
}
|
||||||
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
buffer := make([]byte, test.blocksize, 2*test.blocksize)
|
||||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data[:]), "testbucket", "object", buffer, writeAlgorithm, test.dataBlocks+1)
|
writers := make([]*bitrotWriter, len(disks))
|
||||||
|
for i, disk := range disks {
|
||||||
|
writers[i] = newBitrotWriter(disk, "testbucket", "object", writeAlgorithm)
|
||||||
|
}
|
||||||
|
n, err := storage.CreateFile(context.Background(), bytes.NewReader(data[:]), writers, buffer, storage.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
setup.Remove()
|
setup.Remove()
|
||||||
t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
|
t.Fatalf("Test %d: failed to create erasure test file: %v", i, err)
|
||||||
}
|
}
|
||||||
|
if n != test.data {
|
||||||
|
setup.Remove()
|
||||||
|
t.Fatalf("Test %d: failed to create erasure test file", i)
|
||||||
|
}
|
||||||
|
for i, w := range writers {
|
||||||
|
if w == nil {
|
||||||
|
disks[i] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the checksums of the current part.
|
||||||
|
bitrotReaders := make([]*bitrotReader, len(disks))
|
||||||
|
for index, disk := range disks {
|
||||||
|
if disk == OfflineDisk {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, storage.dataBlocks)
|
||||||
|
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
||||||
|
}
|
||||||
|
|
||||||
writer := bytes.NewBuffer(nil)
|
writer := bytes.NewBuffer(nil)
|
||||||
readInfo, err := storage.ReadFile(context.Background(), writer, "testbucket", "object", test.offset, test.length, test.data, file.Checksums, test.algorithm, test.blocksize)
|
err = storage.ReadFile(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
||||||
if err != nil && !test.shouldFail {
|
if err != nil && !test.shouldFail {
|
||||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -117,25 +142,32 @@ func TestErasureReadFile(t *testing.T) {
|
|||||||
t.Errorf("Test %d: should fail but it passed", i)
|
t.Errorf("Test %d: should fail but it passed", i)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if readInfo.Size != test.length {
|
|
||||||
t.Errorf("Test %d: read returns wrong number of bytes: got: #%d want: #%d", i, readInfo.Size, test.length)
|
|
||||||
}
|
|
||||||
if readInfo.Algorithm != test.algorithm {
|
|
||||||
t.Errorf("Test %d: read returns wrong algorithm: got: %v want: %v", i, readInfo.Algorithm, test.algorithm)
|
|
||||||
}
|
|
||||||
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
|
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
|
||||||
t.Errorf("Test %d: read retruns wrong file content", i)
|
t.Errorf("Test %d: read retruns wrong file content", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for i, r := range bitrotReaders {
|
||||||
|
if r == nil {
|
||||||
|
disks[i] = OfflineDisk
|
||||||
|
}
|
||||||
|
}
|
||||||
if err == nil && !test.shouldFail {
|
if err == nil && !test.shouldFail {
|
||||||
writer.Reset()
|
bitrotReaders = make([]*bitrotReader, len(disks))
|
||||||
for j := range storage.disks[:test.offDisks] {
|
for index, disk := range disks {
|
||||||
storage.disks[j] = badDisk{nil}
|
if disk == OfflineDisk {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endOffset := getErasureShardFileEndOffset(test.offset, test.length, test.data, test.blocksize, storage.dataBlocks)
|
||||||
|
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", writeAlgorithm, endOffset, writers[index].Sum())
|
||||||
|
}
|
||||||
|
for j := range disks[:test.offDisks] {
|
||||||
|
bitrotReaders[j].disk = badDisk{nil}
|
||||||
}
|
}
|
||||||
if test.offDisks > 0 {
|
if test.offDisks > 0 {
|
||||||
storage.disks[0] = OfflineDisk
|
bitrotReaders[0] = nil
|
||||||
}
|
}
|
||||||
readInfo, err = storage.ReadFile(context.Background(), writer, "testbucket", "object", test.offset, test.length, test.data, file.Checksums, test.algorithm, test.blocksize)
|
writer.Reset()
|
||||||
|
err = storage.ReadFile(context.Background(), writer, bitrotReaders, test.offset, test.length, test.data)
|
||||||
if err != nil && !test.shouldFailQuorum {
|
if err != nil && !test.shouldFailQuorum {
|
||||||
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
t.Errorf("Test %d: should pass but failed with: %v", i, err)
|
||||||
}
|
}
|
||||||
@@ -143,12 +175,6 @@ func TestErasureReadFile(t *testing.T) {
|
|||||||
t.Errorf("Test %d: should fail but it passed", i)
|
t.Errorf("Test %d: should fail but it passed", i)
|
||||||
}
|
}
|
||||||
if !test.shouldFailQuorum {
|
if !test.shouldFailQuorum {
|
||||||
if readInfo.Size != test.length {
|
|
||||||
t.Errorf("Test %d: read returns wrong number of bytes: got: #%d want: #%d", i, readInfo.Size, test.length)
|
|
||||||
}
|
|
||||||
if readInfo.Algorithm != test.algorithm {
|
|
||||||
t.Errorf("Test %d: read returns wrong algorithm: got: %v want: %v", i, readInfo.Algorithm, test.algorithm)
|
|
||||||
}
|
|
||||||
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
|
if content := writer.Bytes(); !bytes.Equal(content, data[test.offset:test.offset+test.length]) {
|
||||||
t.Errorf("Test %d: read retruns wrong file content", i)
|
t.Errorf("Test %d: read retruns wrong file content", i)
|
||||||
}
|
}
|
||||||
@@ -174,8 +200,8 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer setup.Remove()
|
defer setup.Remove()
|
||||||
|
disks := setup.disks
|
||||||
storage, err := NewErasureStorage(context.Background(), setup.disks, dataBlocks, parityBlocks, blockSize)
|
storage, err := NewErasureStorage(context.Background(), dataBlocks, parityBlocks, blockSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create ErasureStorage: %v", err)
|
t.Fatalf("failed to create ErasureStorage: %v", err)
|
||||||
}
|
}
|
||||||
@@ -187,17 +213,25 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writers := make([]*bitrotWriter, len(disks))
|
||||||
|
for i, disk := range disks {
|
||||||
|
if disk == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||||
|
}
|
||||||
|
|
||||||
// 10000 iterations with random offsets and lengths.
|
// 10000 iterations with random offsets and lengths.
|
||||||
iterations := 10000
|
iterations := 10000
|
||||||
|
|
||||||
// Create a test file to read from.
|
// Create a test file to read from.
|
||||||
buffer := make([]byte, blockSize, 2*blockSize)
|
buffer := make([]byte, blockSize, 2*blockSize)
|
||||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(data), "testbucket", "testobject", buffer, DefaultBitrotAlgorithm, dataBlocks+1)
|
n, err := storage.CreateFile(context.Background(), bytes.NewReader(data), writers, buffer, storage.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if file.Size != length {
|
if n != length {
|
||||||
t.Errorf("erasureCreateFile returned %d, expected %d", file.Size, length)
|
t.Errorf("erasureCreateFile returned %d, expected %d", n, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// To generate random offset/length.
|
// To generate random offset/length.
|
||||||
@@ -212,7 +246,16 @@ func TestErasureReadFileRandomOffsetLength(t *testing.T) {
|
|||||||
|
|
||||||
expected := data[offset : offset+readLen]
|
expected := data[offset : offset+readLen]
|
||||||
|
|
||||||
_, err = storage.ReadFile(context.Background(), buf, "testbucket", "testobject", offset, readLen, length, file.Checksums, DefaultBitrotAlgorithm, blockSize)
|
// Get the checksums of the current part.
|
||||||
|
bitrotReaders := make([]*bitrotReader, len(disks))
|
||||||
|
for index, disk := range disks {
|
||||||
|
if disk == OfflineDisk {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endOffset := getErasureShardFileEndOffset(offset, readLen, length, blockSize, storage.dataBlocks)
|
||||||
|
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
||||||
|
}
|
||||||
|
err = storage.ReadFile(context.Background(), buf, bitrotReaders, offset, readLen, length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err, offset, readLen)
|
t.Fatal(err, offset, readLen)
|
||||||
}
|
}
|
||||||
@@ -232,31 +275,47 @@ func benchmarkErasureRead(data, parity, dataDown, parityDown int, size int64, b
|
|||||||
b.Fatalf("failed to create test setup: %v", err)
|
b.Fatalf("failed to create test setup: %v", err)
|
||||||
}
|
}
|
||||||
defer setup.Remove()
|
defer setup.Remove()
|
||||||
storage, err := NewErasureStorage(context.Background(), setup.disks, data, parity, blockSizeV1)
|
disks := setup.disks
|
||||||
|
storage, err := NewErasureStorage(context.Background(), data, parity, blockSizeV1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("failed to create ErasureStorage: %v", err)
|
b.Fatalf("failed to create ErasureStorage: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writers := make([]*bitrotWriter, len(disks))
|
||||||
|
for i, disk := range disks {
|
||||||
|
if disk == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writers[i] = newBitrotWriter(disk, "testbucket", "object", DefaultBitrotAlgorithm)
|
||||||
|
}
|
||||||
|
|
||||||
content := make([]byte, size)
|
content := make([]byte, size)
|
||||||
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
buffer := make([]byte, blockSizeV1, 2*blockSizeV1)
|
||||||
file, err := storage.CreateFile(context.Background(), bytes.NewReader(content), "testbucket", "object", buffer, DefaultBitrotAlgorithm, data+1)
|
_, err = storage.CreateFile(context.Background(), bytes.NewReader(content), writers, buffer, storage.dataBlocks+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("failed to create erasure test file: %v", err)
|
b.Fatalf("failed to create erasure test file: %v", err)
|
||||||
}
|
}
|
||||||
checksums := file.Checksums
|
|
||||||
|
|
||||||
for i := 0; i < dataDown; i++ {
|
for i := 0; i < dataDown; i++ {
|
||||||
storage.disks[i] = OfflineDisk
|
writers[i] = nil
|
||||||
}
|
}
|
||||||
for i := data; i < data+parityDown; i++ {
|
for i := data; i < data+parityDown; i++ {
|
||||||
storage.disks[i] = OfflineDisk
|
writers[i] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.SetBytes(size)
|
b.SetBytes(size)
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
if file, err = storage.ReadFile(context.Background(), bytes.NewBuffer(content[:0]), "testbucket", "object", 0, size, size, checksums, DefaultBitrotAlgorithm, blockSizeV1); err != nil {
|
bitrotReaders := make([]*bitrotReader, len(disks))
|
||||||
|
for index, disk := range disks {
|
||||||
|
if writers[index] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endOffset := getErasureShardFileEndOffset(0, size, size, storage.blockSize, storage.dataBlocks)
|
||||||
|
bitrotReaders[index] = newBitrotReader(disk, "testbucket", "object", DefaultBitrotAlgorithm, endOffset, writers[index].Sum())
|
||||||
|
}
|
||||||
|
if err = storage.ReadFile(context.Background(), bytes.NewBuffer(content[:0]), bitrotReaders, 0, size, size); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,3 +104,25 @@ func writeDataBlocks(ctx context.Context, dst io.Writer, enBlocks [][]byte, data
|
|||||||
// Success.
|
// Success.
|
||||||
return totalWritten, nil
|
return totalWritten, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns shard-file size.
|
||||||
|
func getErasureShardFileSize(blockSize int64, totalLength int64, dataBlocks int) int64 {
|
||||||
|
shardSize := ceilFrac(int64(blockSize), int64(dataBlocks))
|
||||||
|
numShards := totalLength / int64(blockSize)
|
||||||
|
lastBlockSize := totalLength % int64(blockSize)
|
||||||
|
lastShardSize := ceilFrac(lastBlockSize, int64(dataBlocks))
|
||||||
|
return shardSize*numShards + lastShardSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the endOffset till which bitrotReader should read data using disk.ReadFile()
|
||||||
|
// partOffset, partLength and partSize are values of the object's part file.
|
||||||
|
func getErasureShardFileEndOffset(partOffset int64, partLength int64, partSize int64, erasureBlockSize int64, dataBlocks int) int64 {
|
||||||
|
shardSize := ceilFrac(erasureBlockSize, int64(dataBlocks))
|
||||||
|
shardFileSize := getErasureShardFileSize(erasureBlockSize, partSize, dataBlocks)
|
||||||
|
endShard := (partOffset + int64(partLength)) / erasureBlockSize
|
||||||
|
endOffset := endShard*shardSize + shardSize
|
||||||
|
if endOffset > shardFileSize {
|
||||||
|
endOffset = shardFileSize
|
||||||
|
}
|
||||||
|
return endOffset
|
||||||
|
}
|
||||||
|
|||||||
+19
-42
@@ -18,8 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
|
||||||
"hash"
|
|
||||||
|
|
||||||
"github.com/klauspost/reedsolomon"
|
"github.com/klauspost/reedsolomon"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
@@ -28,43 +26,36 @@ import (
|
|||||||
// OfflineDisk represents an unavailable disk.
|
// OfflineDisk represents an unavailable disk.
|
||||||
var OfflineDisk StorageAPI // zero value is nil
|
var OfflineDisk StorageAPI // zero value is nil
|
||||||
|
|
||||||
// ErasureFileInfo contains information about an erasure file operation (create, read, heal).
|
// ErasureStorage - erasure encoding details.
|
||||||
type ErasureFileInfo struct {
|
|
||||||
Size int64
|
|
||||||
Algorithm BitrotAlgorithm
|
|
||||||
Checksums [][]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErasureStorage represents an array of disks.
|
|
||||||
// The disks contain erasure coded and bitrot-protected data.
|
|
||||||
type ErasureStorage struct {
|
type ErasureStorage struct {
|
||||||
disks []StorageAPI
|
|
||||||
erasure reedsolomon.Encoder
|
erasure reedsolomon.Encoder
|
||||||
dataBlocks, parityBlocks int
|
dataBlocks, parityBlocks int
|
||||||
|
blockSize int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewErasureStorage creates a new ErasureStorage. The storage erasure codes and protects all data written to
|
// NewErasureStorage creates a new ErasureStorage.
|
||||||
// the disks.
|
func NewErasureStorage(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (s ErasureStorage, err error) {
|
||||||
func NewErasureStorage(ctx context.Context, disks []StorageAPI, dataBlocks, parityBlocks int, blockSize int64) (s ErasureStorage, err error) {
|
shardsize := int(ceilFrac(blockSize, int64(dataBlocks)))
|
||||||
shardsize := (int(blockSize) + dataBlocks - 1) / dataBlocks
|
|
||||||
erasure, err := reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(shardsize))
|
erasure, err := reedsolomon.New(dataBlocks, parityBlocks, reedsolomon.WithAutoGoroutines(shardsize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return s, err
|
return s, err
|
||||||
}
|
}
|
||||||
s = ErasureStorage{
|
s = ErasureStorage{
|
||||||
disks: make([]StorageAPI, len(disks)),
|
|
||||||
erasure: erasure,
|
erasure: erasure,
|
||||||
dataBlocks: dataBlocks,
|
dataBlocks: dataBlocks,
|
||||||
parityBlocks: parityBlocks,
|
parityBlocks: parityBlocks,
|
||||||
|
blockSize: blockSize,
|
||||||
}
|
}
|
||||||
copy(s.disks, disks)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErasureEncode encodes the given data and returns the erasure-coded data.
|
// ErasureEncode encodes the given data and returns the erasure-coded data.
|
||||||
// It returns an error if the erasure coding failed.
|
// It returns an error if the erasure coding failed.
|
||||||
func (s *ErasureStorage) ErasureEncode(ctx context.Context, data []byte) ([][]byte, error) {
|
func (s *ErasureStorage) ErasureEncode(ctx context.Context, data []byte) ([][]byte, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return make([][]byte, s.dataBlocks+s.parityBlocks), nil
|
||||||
|
}
|
||||||
encoded, err := s.erasure.Split(data)
|
encoded, err := s.erasure.Split(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
@@ -81,6 +72,16 @@ func (s *ErasureStorage) ErasureEncode(ctx context.Context, data []byte) ([][]by
|
|||||||
// It only decodes the data blocks but does not verify them.
|
// It only decodes the data blocks but does not verify them.
|
||||||
// It returns an error if the decoding failed.
|
// It returns an error if the decoding failed.
|
||||||
func (s *ErasureStorage) ErasureDecodeDataBlocks(data [][]byte) error {
|
func (s *ErasureStorage) ErasureDecodeDataBlocks(data [][]byte) error {
|
||||||
|
needsReconstruction := false
|
||||||
|
for _, b := range data[:s.dataBlocks] {
|
||||||
|
if b == nil {
|
||||||
|
needsReconstruction = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !needsReconstruction {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err := s.erasure.ReconstructData(data); err != nil {
|
if err := s.erasure.ReconstructData(data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -96,27 +97,3 @@ func (s *ErasureStorage) ErasureDecodeDataAndParityBlocks(ctx context.Context, d
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBitrotVerifier returns a new BitrotVerifier implementing the given algorithm.
|
|
||||||
func NewBitrotVerifier(algorithm BitrotAlgorithm, checksum []byte) *BitrotVerifier {
|
|
||||||
return &BitrotVerifier{algorithm.New(), algorithm, checksum, false}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BitrotVerifier can be used to verify protected data.
|
|
||||||
type BitrotVerifier struct {
|
|
||||||
hash.Hash
|
|
||||||
|
|
||||||
algorithm BitrotAlgorithm
|
|
||||||
sum []byte
|
|
||||||
verified bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify returns true iff the computed checksum of the verifier matches the the checksum provided when the verifier
|
|
||||||
// was created.
|
|
||||||
func (v *BitrotVerifier) Verify() bool {
|
|
||||||
v.verified = true
|
|
||||||
return subtle.ConstantTimeCompare(v.Sum(nil), v.sum) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsVerified returns true iff Verify was called at least once.
|
|
||||||
func (v *BitrotVerifier) IsVerified() bool { return v.verified }
|
|
||||||
|
|||||||
+1
-2
@@ -52,8 +52,7 @@ func TestErasureDecode(t *testing.T) {
|
|||||||
buffer := make([]byte, len(data), 2*len(data))
|
buffer := make([]byte, len(data), 2*len(data))
|
||||||
copy(buffer, data)
|
copy(buffer, data)
|
||||||
|
|
||||||
disks := make([]StorageAPI, test.dataBlocks+test.parityBlocks)
|
storage, err := NewErasureStorage(context.Background(), test.dataBlocks, test.parityBlocks, blockSizeV1)
|
||||||
storage, err := NewErasureStorage(context.Background(), disks, test.dataBlocks, test.parityBlocks, blockSizeV1)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to create erasure storage: %v", i, err)
|
t.Fatalf("Test %d: failed to create erasure storage: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|||||||
+114
-12
@@ -24,6 +24,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/lock"
|
"github.com/minio/minio/pkg/lock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,24 +56,22 @@ type formatFSVersionDetect struct {
|
|||||||
} `json:"fs"`
|
} `json:"fs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic structure to manage both v1 and v2 structures
|
||||||
|
type formatFS struct {
|
||||||
|
formatMetaV1
|
||||||
|
FS interface{} `json:"fs"`
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the latest "fs" format V1
|
// Returns the latest "fs" format V1
|
||||||
func newFormatFSV1() (format *formatFSV1) {
|
func newFormatFSV1() (format *formatFSV1) {
|
||||||
f := &formatFSV1{}
|
f := &formatFSV1{}
|
||||||
f.Version = formatMetaVersionV1
|
f.Version = formatMetaVersionV1
|
||||||
f.Format = formatBackendFS
|
f.Format = formatBackendFS
|
||||||
|
f.ID = mustGetUUID()
|
||||||
f.FS.Version = formatFSVersionV1
|
f.FS.Version = formatFSVersionV1
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the latest "fs" format V2
|
|
||||||
func newFormatFSV2() (format *formatFSV2) {
|
|
||||||
f := &formatFSV2{}
|
|
||||||
f.Version = formatMetaVersionV1
|
|
||||||
f.Format = formatBackendFS
|
|
||||||
f.FS.Version = formatFSVersionV2
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the field formatMetaV1.Format i.e the string "fs" which is never likely to change.
|
// Returns the field formatMetaV1.Format i.e the string "fs" which is never likely to change.
|
||||||
// We do not use this function in XL to get the format as the file is not fcntl-locked on XL.
|
// We do not use this function in XL to get the format as the file is not fcntl-locked on XL.
|
||||||
func formatMetaGetFormatBackendFS(r io.ReadSeeker) (string, error) {
|
func formatMetaGetFormatBackendFS(r io.ReadSeeker) (string, error) {
|
||||||
@@ -115,7 +114,16 @@ func formatFSMigrateV1ToV2(ctx context.Context, wlk *lock.LockedFile, fsPath str
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonSave(wlk.File, newFormatFSV2())
|
formatV1 := formatFSV1{}
|
||||||
|
if err = jsonLoad(wlk, &formatV1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
formatV2 := formatFSV2{}
|
||||||
|
formatV2.formatMetaV1 = formatV1.formatMetaV1
|
||||||
|
formatV2.FS.Version = formatFSVersionV2
|
||||||
|
|
||||||
|
return jsonSave(wlk.File, formatV2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate the "fs" backend.
|
// Migrate the "fs" backend.
|
||||||
@@ -179,6 +187,12 @@ func createFormatFS(ctx context.Context, fsFormatPath string) error {
|
|||||||
// migrate the backend when we are actively working on the backend.
|
// migrate the backend when we are actively working on the backend.
|
||||||
func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, err error) {
|
func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, err error) {
|
||||||
fsFormatPath := pathJoin(fsPath, minioMetaBucket, formatConfigFile)
|
fsFormatPath := pathJoin(fsPath, minioMetaBucket, formatConfigFile)
|
||||||
|
|
||||||
|
// Add a deployment ID, if it does not exist.
|
||||||
|
if err := formatFSFixDeploymentID(fsFormatPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Any read on format.json should be done with read-lock.
|
// Any read on format.json should be done with read-lock.
|
||||||
// Any write on format.json should be done with write-lock.
|
// Any write on format.json should be done with write-lock.
|
||||||
for {
|
for {
|
||||||
@@ -235,7 +249,8 @@ func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, er
|
|||||||
rlk.Close()
|
rlk.Close()
|
||||||
// Hold write lock during migration so that we do not disturb any
|
// Hold write lock during migration so that we do not disturb any
|
||||||
// minio processes running in parallel.
|
// minio processes running in parallel.
|
||||||
wlk, err := lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
|
var wlk *lock.LockedFile
|
||||||
|
wlk, err = lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
|
||||||
if err == lock.ErrAlreadyLocked {
|
if err == lock.ErrAlreadyLocked {
|
||||||
// Lock already present, sleep and attempt again.
|
// Lock already present, sleep and attempt again.
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
@@ -253,7 +268,94 @@ func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, er
|
|||||||
// Successfully migrated, now try to hold a read-lock on format.json
|
// Successfully migrated, now try to hold a read-lock on format.json
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
var id string
|
||||||
|
if id, err = formatFSGetDeploymentID(rlk); err != nil {
|
||||||
|
rlk.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger.SetDeploymentID(id)
|
||||||
return rlk, nil
|
return rlk, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatFSGetDeploymentID(rlk *lock.RLockedFile) (id string, err error) {
|
||||||
|
format := &formatFS{}
|
||||||
|
if err := jsonLoad(rlk, format); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return format.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a deployment ID if one does not exist already.
|
||||||
|
func formatFSFixDeploymentID(fsFormatPath string) error {
|
||||||
|
rlk, err := lock.RLockedOpenFile(fsFormatPath)
|
||||||
|
if err == nil {
|
||||||
|
// format.json can be empty in a rare condition when another
|
||||||
|
// minio process just created the file but could not hold lock
|
||||||
|
// and write to it.
|
||||||
|
var fi os.FileInfo
|
||||||
|
fi, err = rlk.Stat()
|
||||||
|
if err != nil {
|
||||||
|
rlk.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fi.Size() == 0 {
|
||||||
|
rlk.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
formatBackend, err := formatMetaGetFormatBackendFS(rlk)
|
||||||
|
if err != nil {
|
||||||
|
rlk.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if formatBackend != formatBackendFS {
|
||||||
|
rlk.Close()
|
||||||
|
return fmt.Errorf(`%s file: expected format-type: %s, found: %s`, formatConfigFile, formatBackendFS, formatBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
format := &formatFS{}
|
||||||
|
err = jsonLoad(rlk, format)
|
||||||
|
rlk.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it needs to be updated
|
||||||
|
if format.ID != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
wlk, err := lock.TryLockedOpenFile(fsFormatPath, os.O_RDWR, 0)
|
||||||
|
if err == lock.ErrAlreadyLocked {
|
||||||
|
// Lock already present, sleep and attempt again.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer wlk.Close()
|
||||||
|
|
||||||
|
err = jsonLoad(wlk, format)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it needs to be updated
|
||||||
|
if format.ID != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
format.ID = mustGetUUID()
|
||||||
|
return jsonSave(wlk, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,4 +49,6 @@ type formatMetaV1 struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
// Format indicates the backend format type, supports two values 'xl' and 'fs'.
|
// Format indicates the backend format type, supports two values 'xl' and 'fs'.
|
||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
|
// ID is the identifier for the minio deployment
|
||||||
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-9
@@ -21,12 +21,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
|
"github.com/minio/minio/cmd/logger"
|
||||||
sha256 "github.com/minio/sha256-simd"
|
sha256 "github.com/minio/sha256-simd"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,10 +87,11 @@ type formatXLV1 struct {
|
|||||||
// Represents the V2 backend disk structure version
|
// Represents the V2 backend disk structure version
|
||||||
// under `.minio.sys` and actual data namespace.
|
// under `.minio.sys` and actual data namespace.
|
||||||
// formatXLV2 - structure holds format config version '2'.
|
// formatXLV2 - structure holds format config version '2'.
|
||||||
|
// The V2 format to support "large bucket" support where a bucket
|
||||||
|
// can span multiple erasure sets.
|
||||||
type formatXLV2 struct {
|
type formatXLV2 struct {
|
||||||
Version string `json:"version"`
|
formatMetaV1
|
||||||
Format string `json:"format"`
|
XL struct {
|
||||||
XL struct {
|
|
||||||
Version string `json:"version"` // Version of 'xl' format.
|
Version string `json:"version"` // Version of 'xl' format.
|
||||||
This string `json:"this"` // This field carries assigned disk uuid.
|
This string `json:"this"` // This field carries assigned disk uuid.
|
||||||
// Sets field carries the input disk order generated the first
|
// Sets field carries the input disk order generated the first
|
||||||
@@ -107,9 +109,8 @@ type formatXLV2 struct {
|
|||||||
// In .minio.sys/multipart we have:
|
// In .minio.sys/multipart we have:
|
||||||
// sha256(bucket/object)/uploadID/[xl.json, part.1, part.2 ....]
|
// sha256(bucket/object)/uploadID/[xl.json, part.1, part.2 ....]
|
||||||
type formatXLV3 struct {
|
type formatXLV3 struct {
|
||||||
Version string `json:"version"`
|
formatMetaV1
|
||||||
Format string `json:"format"`
|
XL struct {
|
||||||
XL struct {
|
|
||||||
Version string `json:"version"` // Version of 'xl' format.
|
Version string `json:"version"` // Version of 'xl' format.
|
||||||
This string `json:"this"` // This field carries assigned disk uuid.
|
This string `json:"this"` // This field carries assigned disk uuid.
|
||||||
// Sets field carries the input disk order generated the first
|
// Sets field carries the input disk order generated the first
|
||||||
@@ -127,6 +128,7 @@ func newFormatXLV3(numSets int, setLen int) *formatXLV3 {
|
|||||||
format := &formatXLV3{}
|
format := &formatXLV3{}
|
||||||
format.Version = formatMetaVersionV1
|
format.Version = formatMetaVersionV1
|
||||||
format.Format = formatBackendXL
|
format.Format = formatBackendXL
|
||||||
|
format.ID = mustGetUUID()
|
||||||
format.XL.Version = formatXLVersionV3
|
format.XL.Version = formatXLVersionV3
|
||||||
format.XL.DistributionAlgo = formatXLVersionV2DistributionAlgo
|
format.XL.DistributionAlgo = formatXLVersionV2DistributionAlgo
|
||||||
format.XL.Sets = make([][]string, numSets)
|
format.XL.Sets = make([][]string, numSets)
|
||||||
@@ -264,10 +266,11 @@ func formatXLMigrateV2ToV3(export string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = os.RemoveAll(pathJoin(export, minioMetaMultipartBucket)); err != nil {
|
if err = removeAll(pathJoin(export, minioMetaMultipartBucket)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = os.MkdirAll(pathJoin(export, minioMetaMultipartBucket), 0755); err != nil {
|
|
||||||
|
if err = mkdirAll(pathJoin(export, minioMetaMultipartBucket), 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +446,112 @@ func checkFormatXLValues(formats []*formatXLV3) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get Deployment ID for the XL sets from format.json.
|
||||||
|
// This need not be in quorum. Even if one of the format.json
|
||||||
|
// file has this value, we assume it is valid.
|
||||||
|
// If more than one format.json's have different id, it is considered a corrupt
|
||||||
|
// backend format.
|
||||||
|
func formatXLGetDeploymentID(refFormat *formatXLV3, formats []*formatXLV3) (string, error) {
|
||||||
|
var deploymentID string
|
||||||
|
for _, format := range formats {
|
||||||
|
if format == nil || format.ID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(format.XL.Sets, refFormat.XL.Sets) {
|
||||||
|
// Found an ID in one of the format.json file
|
||||||
|
// Set deploymentID for the first time.
|
||||||
|
if deploymentID == "" {
|
||||||
|
deploymentID = format.ID
|
||||||
|
} else if deploymentID != format.ID {
|
||||||
|
// DeploymentID found earlier doesn't match with the
|
||||||
|
// current format.json's ID.
|
||||||
|
return "", errCorruptedFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deploymentID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatXLFixDeploymentID - Add deployment id if it is not present.
|
||||||
|
func formatXLFixDeploymentID(ctx context.Context, storageDisks []StorageAPI, refFormat *formatXLV3) (err error) {
|
||||||
|
// Acquire lock on format.json
|
||||||
|
mutex := newNSLock(globalIsDistXL)
|
||||||
|
formatLock := mutex.NewNSLock(minioMetaBucket, formatConfigFile)
|
||||||
|
if err = formatLock.GetLock(globalHealingTimeout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer formatLock.Unlock()
|
||||||
|
|
||||||
|
// Attempt to load all `format.json` from all disks.
|
||||||
|
var sErrs []error
|
||||||
|
formats, sErrs := loadFormatXLAll(storageDisks)
|
||||||
|
for i, sErr := range sErrs {
|
||||||
|
if _, ok := formatCriticalErrors[sErr]; ok {
|
||||||
|
return fmt.Errorf("Disk %s: %s", globalEndpoints[i], sErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := range formats {
|
||||||
|
// If the XL sets do not match, set those formats to nil,
|
||||||
|
// We do not have to update the ID on those format.json file.
|
||||||
|
if formats[index] != nil && !reflect.DeepEqual(formats[index].XL.Sets, refFormat.XL.Sets) {
|
||||||
|
formats[index] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refFormat.ID, err = formatXLGetDeploymentID(refFormat, formats)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ID is set, then some other node got the lock
|
||||||
|
// before this node could and generated an ID
|
||||||
|
// for the deployment. No need to generate one.
|
||||||
|
if refFormat.ID != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID is generated for the first time,
|
||||||
|
// We set the ID in all the formats and update.
|
||||||
|
refFormat.ID = mustGetUUID()
|
||||||
|
for _, format := range formats {
|
||||||
|
if format != nil {
|
||||||
|
format.ID = refFormat.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Deployment ID needs to be set on all the disks.
|
||||||
|
// Save `format.json` across all disks.
|
||||||
|
return saveFormatXLAll(ctx, storageDisks, formats)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the valid local disks which have not been updated before.
|
||||||
|
func formatXLFixLocalDeploymentID(ctx context.Context, storageDisks []StorageAPI, refFormat *formatXLV3) error {
|
||||||
|
// If this server was down when the deploymentID was updated
|
||||||
|
// then we make sure that we update the local disks with the deploymentID.
|
||||||
|
for index, storageDisk := range storageDisks {
|
||||||
|
if globalEndpoints[index].IsLocal && storageDisk != nil && storageDisk.IsOnline() {
|
||||||
|
format, err := loadFormatXL(storageDisk)
|
||||||
|
if err != nil {
|
||||||
|
// Disk can be offline etc.
|
||||||
|
// ignore the errors seen here.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if format.ID != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(format.XL.Sets, refFormat.XL.Sets) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
format.ID = refFormat.ID
|
||||||
|
if err := saveFormatXL(storageDisk, format); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return fmt.Errorf("Unable to save format.json, %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Get backend XL format in quorum `format.json`.
|
// Get backend XL format in quorum `format.json`.
|
||||||
func getFormatXLInQuorum(formats []*formatXLV3) (*formatXLV3, error) {
|
func getFormatXLInQuorum(formats []*formatXLV3) (*formatXLV3, error) {
|
||||||
formatHashes := make([]string, len(formats))
|
formatHashes := make([]string, len(formats))
|
||||||
|
|||||||
+67
-6
@@ -345,8 +345,10 @@ func TestCheckFormatXLValue(t *testing.T) {
|
|||||||
// Invalid XL format version "2".
|
// Invalid XL format version "2".
|
||||||
{
|
{
|
||||||
&formatXLV3{
|
&formatXLV3{
|
||||||
Version: "2",
|
formatMetaV1: formatMetaV1{
|
||||||
Format: "XL",
|
Version: "2",
|
||||||
|
Format: "XL",
|
||||||
|
},
|
||||||
XL: struct {
|
XL: struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
This string `json:"this"`
|
This string `json:"this"`
|
||||||
@@ -361,8 +363,10 @@ func TestCheckFormatXLValue(t *testing.T) {
|
|||||||
// Invalid XL format "Unknown".
|
// Invalid XL format "Unknown".
|
||||||
{
|
{
|
||||||
&formatXLV3{
|
&formatXLV3{
|
||||||
Version: "1",
|
formatMetaV1: formatMetaV1{
|
||||||
Format: "Unknown",
|
Version: "1",
|
||||||
|
Format: "Unknown",
|
||||||
|
},
|
||||||
XL: struct {
|
XL: struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
This string `json:"this"`
|
This string `json:"this"`
|
||||||
@@ -377,8 +381,10 @@ func TestCheckFormatXLValue(t *testing.T) {
|
|||||||
// Invalid XL format version "0".
|
// Invalid XL format version "0".
|
||||||
{
|
{
|
||||||
&formatXLV3{
|
&formatXLV3{
|
||||||
Version: "1",
|
formatMetaV1: formatMetaV1{
|
||||||
Format: "XL",
|
Version: "1",
|
||||||
|
Format: "XL",
|
||||||
|
},
|
||||||
XL: struct {
|
XL: struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
This string `json:"this"`
|
This string `json:"this"`
|
||||||
@@ -466,6 +472,61 @@ func TestGetFormatXLInQuorumCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests formatXLGetDeploymentID()
|
||||||
|
func TestGetXLID(t *testing.T) {
|
||||||
|
setCount := 2
|
||||||
|
disksPerSet := 8
|
||||||
|
|
||||||
|
format := newFormatXLV3(setCount, disksPerSet)
|
||||||
|
formats := make([]*formatXLV3, 16)
|
||||||
|
|
||||||
|
for i := 0; i < setCount; i++ {
|
||||||
|
for j := 0; j < disksPerSet; j++ {
|
||||||
|
newFormat := *format
|
||||||
|
newFormat.XL.This = format.XL.Sets[i][j]
|
||||||
|
formats[i*disksPerSet+j] = &newFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a format from list of formats in quorum.
|
||||||
|
quorumFormat, err := getFormatXLInQuorum(formats)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the reference format and input formats are same.
|
||||||
|
var id string
|
||||||
|
if id, err = formatXLGetDeploymentID(quorumFormat, formats); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
t.Fatal("ID cannot be empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
formats[0] = nil
|
||||||
|
if id, err = formatXLGetDeploymentID(quorumFormat, formats); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if id == "" {
|
||||||
|
t.Fatal("ID cannot be empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
formats[1].XL.Sets[0][0] = "bad-uuid"
|
||||||
|
if id, err = formatXLGetDeploymentID(quorumFormat, formats); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
t.Fatal("ID cannot be empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
formats[2].ID = "bad-id"
|
||||||
|
if id, err = formatXLGetDeploymentID(quorumFormat, formats); err != errCorruptedFormat {
|
||||||
|
t.Fatal("Unexpected Success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize new format sets.
|
// Initialize new format sets.
|
||||||
func TestNewFormatSets(t *testing.T) {
|
func TestNewFormatSets(t *testing.T) {
|
||||||
setCount := 2
|
setCount := 2
|
||||||
|
|||||||
+12
-8
@@ -64,7 +64,7 @@ func fsRemoveAll(ctx context.Context, dirPath string) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = os.RemoveAll(dirPath); err != nil {
|
if err = removeAll(dirPath); err != nil {
|
||||||
if os.IsPermission(err) {
|
if os.IsPermission(err) {
|
||||||
logger.LogIf(ctx, errVolumeAccessDenied)
|
logger.LogIf(ctx, errVolumeAccessDenied)
|
||||||
return errVolumeAccessDenied
|
return errVolumeAccessDenied
|
||||||
@@ -121,23 +121,25 @@ func fsMkdir(ctx context.Context, dirPath string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = os.Mkdir((dirPath), 0777); err != nil {
|
if err = os.Mkdir((dirPath), 0777); err != nil {
|
||||||
if os.IsExist(err) {
|
switch {
|
||||||
|
case os.IsExist(err):
|
||||||
return errVolumeExists
|
return errVolumeExists
|
||||||
} else if os.IsPermission(err) {
|
case os.IsPermission(err):
|
||||||
logger.LogIf(ctx, errDiskAccessDenied)
|
logger.LogIf(ctx, errDiskAccessDenied)
|
||||||
return errDiskAccessDenied
|
return errDiskAccessDenied
|
||||||
} else if isSysErrNotDir(err) {
|
case isSysErrNotDir(err):
|
||||||
// File path cannot be verified since
|
// File path cannot be verified since
|
||||||
// one of the parents is a file.
|
// one of the parents is a file.
|
||||||
logger.LogIf(ctx, errDiskAccessDenied)
|
logger.LogIf(ctx, errDiskAccessDenied)
|
||||||
return errDiskAccessDenied
|
return errDiskAccessDenied
|
||||||
} else if isSysErrPathNotFound(err) {
|
case isSysErrPathNotFound(err):
|
||||||
// Add specific case for windows.
|
// Add specific case for windows.
|
||||||
logger.LogIf(ctx, errDiskAccessDenied)
|
logger.LogIf(ctx, errDiskAccessDenied)
|
||||||
return errDiskAccessDenied
|
return errDiskAccessDenied
|
||||||
|
default:
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -343,7 +345,9 @@ func fsCreateFile(ctx context.Context, filePath string, reader io.Reader, buf []
|
|||||||
if buf != nil {
|
if buf != nil {
|
||||||
bytesWritten, err = io.CopyBuffer(writer, reader, buf)
|
bytesWritten, err = io.CopyBuffer(writer, reader, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
if err != io.ErrUnexpectedEOF {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-15
@@ -543,25 +543,11 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
|
|
||||||
// All parts except the last part has to be atleast 5MB.
|
// All parts except the last part has to be atleast 5MB.
|
||||||
if !isMinAllowedPartSize(fi.Size()) {
|
if !isMinAllowedPartSize(fi.Size()) {
|
||||||
err = PartTooSmall{
|
return oi, PartTooSmall{
|
||||||
PartNumber: part.PartNumber,
|
PartNumber: part.PartNumber,
|
||||||
PartSize: fi.Size(),
|
PartSize: fi.Size(),
|
||||||
PartETag: part.ETag,
|
PartETag: part.ETag,
|
||||||
}
|
}
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return oi, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Make necessary changes in future as explained in the below comment.
|
|
||||||
// All parts except the last part has to be of same size. We are introducing this
|
|
||||||
// check to see if any clients break. If clients do not break then we can optimize
|
|
||||||
// multipart PutObjectPart by writing the part at the right offset using pwrite()
|
|
||||||
// so that we don't need to do background append at all. i.e by the time we get
|
|
||||||
// CompleteMultipartUpload we already have the full file available which can be
|
|
||||||
// renamed to the main name-space.
|
|
||||||
if partSize != fi.Size() {
|
|
||||||
logger.LogIf(ctx, PartsSizeUnequal{})
|
|
||||||
return oi, PartsSizeUnequal{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,23 +90,18 @@ func TestNewMultipartUploadFaultyDisk(t *testing.T) {
|
|||||||
|
|
||||||
// TestPutObjectPartFaultyDisk - test PutObjectPart with faulty disks
|
// TestPutObjectPartFaultyDisk - test PutObjectPart with faulty disks
|
||||||
func TestPutObjectPartFaultyDisk(t *testing.T) {
|
func TestPutObjectPartFaultyDisk(t *testing.T) {
|
||||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(root)
|
|
||||||
|
|
||||||
// Prepare for tests
|
// Prepare for tests
|
||||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||||
defer os.RemoveAll(disk)
|
defer os.RemoveAll(disk)
|
||||||
obj := initFSObjects(disk, t)
|
obj := initFSObjects(disk, t)
|
||||||
|
|
||||||
fs := obj.(*FSObjects)
|
fs := obj.(*FSObjects)
|
||||||
bucketName := "bucket"
|
bucketName := "bucket"
|
||||||
objectName := "object"
|
objectName := "object"
|
||||||
data := []byte("12345")
|
data := []byte("12345")
|
||||||
dataLen := int64(len(data))
|
dataLen := int64(len(data))
|
||||||
|
|
||||||
if err = obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
if err := obj.MakeBucketWithLocation(context.Background(), bucketName, ""); err != nil {
|
||||||
t.Fatal("Cannot create bucket, err: ", err)
|
t.Fatal("Cannot create bucket, err: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-14
@@ -93,18 +93,20 @@ func (fsi *fsIOPool) Open(path string) (*lock.RLockedFile, error) {
|
|||||||
// Open file for reading with read lock.
|
// Open file for reading with read lock.
|
||||||
newRlkFile, err := lock.RLockedOpenFile(path)
|
newRlkFile, err := lock.RLockedOpenFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
switch {
|
||||||
|
case os.IsNotExist(err):
|
||||||
return nil, errFileNotFound
|
return nil, errFileNotFound
|
||||||
} else if os.IsPermission(err) {
|
case os.IsPermission(err):
|
||||||
return nil, errFileAccessDenied
|
return nil, errFileAccessDenied
|
||||||
} else if isSysErrIsDir(err) {
|
case isSysErrIsDir(err):
|
||||||
return nil, errIsNotRegular
|
return nil, errIsNotRegular
|
||||||
} else if isSysErrNotDir(err) {
|
case isSysErrNotDir(err):
|
||||||
return nil, errFileAccessDenied
|
return nil, errFileAccessDenied
|
||||||
} else if isSysErrPathNotFound(err) {
|
case isSysErrPathNotFound(err):
|
||||||
return nil, errFileNotFound
|
return nil, errFileNotFound
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save new reader on the map.
|
/// Save new reader on the map.
|
||||||
@@ -148,14 +150,16 @@ func (fsi *fsIOPool) Write(path string) (wlk *lock.LockedFile, err error) {
|
|||||||
|
|
||||||
wlk, err = lock.LockedOpenFile(path, os.O_RDWR, 0666)
|
wlk, err = lock.LockedOpenFile(path, os.O_RDWR, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
switch {
|
||||||
|
case os.IsNotExist(err):
|
||||||
return nil, errFileNotFound
|
return nil, errFileNotFound
|
||||||
} else if os.IsPermission(err) {
|
case os.IsPermission(err):
|
||||||
return nil, errFileAccessDenied
|
return nil, errFileAccessDenied
|
||||||
} else if isSysErrIsDir(err) {
|
case isSysErrIsDir(err):
|
||||||
return nil, errIsNotRegular
|
return nil, errIsNotRegular
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return wlk, nil
|
return wlk, nil
|
||||||
}
|
}
|
||||||
@@ -175,14 +179,16 @@ func (fsi *fsIOPool) Create(path string) (wlk *lock.LockedFile, err error) {
|
|||||||
// Attempt to create the file.
|
// Attempt to create the file.
|
||||||
wlk, err = lock.LockedOpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
wlk, err = lock.LockedOpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsPermission(err) {
|
switch {
|
||||||
|
case os.IsPermission(err):
|
||||||
return nil, errFileAccessDenied
|
return nil, errFileAccessDenied
|
||||||
} else if isSysErrIsDir(err) {
|
case isSysErrIsDir(err):
|
||||||
return nil, errIsNotRegular
|
return nil, errIsNotRegular
|
||||||
} else if isSysErrPathNotFound(err) {
|
case isSysErrPathNotFound(err):
|
||||||
return nil, errFileAccessDenied
|
return nil, errFileAccessDenied
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
|
|||||||
+11
-26
@@ -151,16 +151,6 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
|||||||
// or cause changes on backend format.
|
// or cause changes on backend format.
|
||||||
fs.fsFormatRlk = rlk
|
fs.fsFormatRlk = rlk
|
||||||
|
|
||||||
// Initialize notification system.
|
|
||||||
if err = globalNotificationSys.Init(fs); err != nil {
|
|
||||||
return nil, uiErrUnableToReadFromBackend(err).Msg("Unable to initialize notification system")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize policy system.
|
|
||||||
if err = globalPolicySys.Init(fs); err != nil {
|
|
||||||
return nil, uiErrUnableToReadFromBackend(err).Msg("Unable to initialize policy system")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fs.diskMount {
|
if !fs.diskMount {
|
||||||
go fs.diskUsage(globalServiceDoneCh)
|
go fs.diskUsage(globalServiceDoneCh)
|
||||||
}
|
}
|
||||||
@@ -275,18 +265,6 @@ func (fs *FSObjects) StorageInfo(ctx context.Context) StorageInfo {
|
|||||||
return storageInfo
|
return storageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locking operations
|
|
||||||
|
|
||||||
// ListLocks - List namespace locks held in object layer
|
|
||||||
func (fs *FSObjects) ListLocks(ctx context.Context, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
|
||||||
return []VolumeLockInfo{}, NotImplemented{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearLocks - Clear namespace locks held in object layer
|
|
||||||
func (fs *FSObjects) ClearLocks(ctx context.Context, info []VolumeLockInfo) error {
|
|
||||||
return NotImplemented{}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bucket operations
|
/// Bucket operations
|
||||||
|
|
||||||
// getBucketDir - will convert incoming bucket names to
|
// getBucketDir - will convert incoming bucket names to
|
||||||
@@ -978,13 +956,15 @@ func (fs *FSObjects) DeleteObject(ctx context.Context, bucket, object string) er
|
|||||||
// is a leaf or non-leaf entry.
|
// is a leaf or non-leaf entry.
|
||||||
func (fs *FSObjects) listDirFactory(isLeaf isLeafFunc) listDirFunc {
|
func (fs *FSObjects) listDirFactory(isLeaf isLeafFunc) listDirFunc {
|
||||||
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
||||||
listDir := func(bucket, prefixDir, prefixEntry string) (entries []string, delayIsLeaf bool, err error) {
|
listDir := func(bucket, prefixDir, prefixEntry string) (entries []string, delayIsLeaf bool) {
|
||||||
|
var err error
|
||||||
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
logger.LogIf(context.Background(), err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
entries, delayIsLeaf = filterListEntries(bucket, prefixDir, entries, prefixEntry, isLeaf)
|
entries, delayIsLeaf = filterListEntries(bucket, prefixDir, entries, prefixEntry, isLeaf)
|
||||||
return entries, delayIsLeaf, nil
|
return entries, delayIsLeaf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return list factory instance.
|
// Return list factory instance.
|
||||||
@@ -1250,7 +1230,12 @@ func (fs *FSObjects) DeleteBucketPolicy(ctx context.Context, bucket string) erro
|
|||||||
|
|
||||||
// ListObjectsV2 lists all blobs in bucket filtered by prefix
|
// ListObjectsV2 lists all blobs in bucket filtered by prefix
|
||||||
func (fs *FSObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error) {
|
func (fs *FSObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error) {
|
||||||
loi, err := fs.ListObjects(ctx, bucket, prefix, continuationToken, delimiter, maxKeys)
|
marker := continuationToken
|
||||||
|
if marker == "" {
|
||||||
|
marker = startAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
loi, err := fs.ListObjects(ctx, bucket, prefix, marker, delimiter, maxKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-12
@@ -26,12 +26,6 @@ import (
|
|||||||
|
|
||||||
// Tests for if parent directory is object
|
// Tests for if parent directory is object
|
||||||
func TestFSParentDirIsObject(t *testing.T) {
|
func TestFSParentDirIsObject(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
obj, disk, err := prepareFS()
|
obj, disk, err := prepareFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -120,12 +114,6 @@ func TestNewFS(t *testing.T) {
|
|||||||
// TestFSShutdown - initialize a new FS object layer then calls
|
// TestFSShutdown - initialize a new FS object layer then calls
|
||||||
// Shutdown to check returned results
|
// Shutdown to check returned results
|
||||||
func TestFSShutdown(t *testing.T) {
|
func TestFSShutdown(t *testing.T) {
|
||||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(rootPath)
|
|
||||||
|
|
||||||
bucketName := "testbucket"
|
bucketName := "testbucket"
|
||||||
objectName := "object"
|
objectName := "object"
|
||||||
// Create and return an fsObject with its path in the disk
|
// Create and return an fsObject with its path in the disk
|
||||||
@@ -133,6 +121,7 @@ func TestFSShutdown(t *testing.T) {
|
|||||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||||
obj := initFSObjects(disk, t)
|
obj := initFSObjects(disk, t)
|
||||||
fs := obj.(*FSObjects)
|
fs := obj.(*FSObjects)
|
||||||
|
|
||||||
objectContent := "12345"
|
objectContent := "12345"
|
||||||
obj.MakeBucketWithLocation(context.Background(), bucketName, "")
|
obj.MakeBucketWithLocation(context.Background(), bucketName, "")
|
||||||
obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil)
|
obj.PutObject(context.Background(), bucketName, objectName, mustGetHashReader(t, bytes.NewReader([]byte(objectContent)), int64(len(objectContent)), "", ""), nil)
|
||||||
|
|||||||
+16
-11
@@ -150,14 +150,25 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
|||||||
|
|
||||||
// Validate if we have access, secret set through environment.
|
// Validate if we have access, secret set through environment.
|
||||||
if !globalIsEnvCreds {
|
if !globalIsEnvCreds {
|
||||||
logger.Fatal(uiErrEnvCredentialsMissing(nil), "Unable to start gateway")
|
logger.Fatal(uiErrEnvCredentialsMissingGateway(nil), "Unable to start gateway")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create certs path.
|
// Create certs path.
|
||||||
logger.FatalIf(createConfigDir(), "Unable to create configuration directories")
|
logger.FatalIf(createConfigDir(), "Unable to create configuration directories")
|
||||||
|
|
||||||
// Initialize gateway config.
|
// Initialize server config.
|
||||||
initConfig()
|
srvCfg := newServerConfig()
|
||||||
|
|
||||||
|
// Override any values from ENVs.
|
||||||
|
srvCfg.loadFromEnvs()
|
||||||
|
|
||||||
|
// Load values to cached global values.
|
||||||
|
srvCfg.loadToCachedConfigs()
|
||||||
|
|
||||||
|
// hold the mutex lock before a new config is assigned.
|
||||||
|
globalServerConfigMu.Lock()
|
||||||
|
globalServerConfig = srvCfg
|
||||||
|
globalServerConfigMu.Unlock()
|
||||||
|
|
||||||
// Check and load SSL certificates.
|
// Check and load SSL certificates.
|
||||||
var err error
|
var err error
|
||||||
@@ -170,8 +181,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
|||||||
initNSLock(false) // Enable local namespace lock.
|
initNSLock(false) // Enable local namespace lock.
|
||||||
|
|
||||||
// Create new notification system.
|
// Create new notification system.
|
||||||
globalNotificationSys, err = NewNotificationSys(globalServerConfig, EndpointList{})
|
globalNotificationSys = NewNotificationSys(globalServerConfig, EndpointList{})
|
||||||
logger.FatalIf(err, "Unable to create new notification system")
|
|
||||||
|
|
||||||
// Create new policy system.
|
// Create new policy system.
|
||||||
globalPolicySys = NewPolicySys()
|
globalPolicySys = NewPolicySys()
|
||||||
@@ -215,12 +225,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
|||||||
logger.FatalIf(err, "Unable to initialize gateway backend")
|
logger.FatalIf(err, "Unable to initialize gateway backend")
|
||||||
}
|
}
|
||||||
|
|
||||||
if gw.Name() != "nas" {
|
go globalPolicySys.Init(newObject)
|
||||||
// Initialize policy sys for all gateways. NAS gateway already
|
|
||||||
// initializes policy sys internally, avoid double initialization.
|
|
||||||
// Additionally also don't block the initialization of gateway.
|
|
||||||
go globalPolicySys.Init(newObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once endpoints are finalized, initialize the new object api.
|
// Once endpoints are finalized, initialize the new object api.
|
||||||
globalObjLayerMutex.Lock()
|
globalObjLayerMutex.Lock()
|
||||||
|
|||||||
@@ -23,11 +23,14 @@ import (
|
|||||||
|
|
||||||
// Test printing Gateway common message.
|
// Test printing Gateway common message.
|
||||||
func TestPrintGatewayCommonMessage(t *testing.T) {
|
func TestPrintGatewayCommonMessage(t *testing.T) {
|
||||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
obj, fsDir, err := prepareFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(fsDir)
|
||||||
|
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
||||||
printGatewayCommonMsg(apiEndpoints)
|
printGatewayCommonMsg(apiEndpoints)
|
||||||
@@ -35,11 +38,14 @@ func TestPrintGatewayCommonMessage(t *testing.T) {
|
|||||||
|
|
||||||
// Test print gateway startup message.
|
// Test print gateway startup message.
|
||||||
func TestPrintGatewayStartupMessage(t *testing.T) {
|
func TestPrintGatewayStartupMessage(t *testing.T) {
|
||||||
root, err := newTestConfig(globalMinioDefaultRegion)
|
obj, fsDir, err := prepareFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(root)
|
defer os.RemoveAll(fsDir)
|
||||||
|
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
||||||
printGatewayStartupMessage(apiEndpoints, "azure")
|
printGatewayStartupMessage(apiEndpoints, "azure")
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
@@ -137,20 +136,6 @@ func (a GatewayUnsupported) CopyObject(ctx context.Context, srcBucket string, sr
|
|||||||
return objInfo, NotImplemented{}
|
return objInfo, NotImplemented{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locking operations
|
|
||||||
|
|
||||||
// ListLocks lists namespace locks held in object layer
|
|
||||||
func (a GatewayUnsupported) ListLocks(ctx context.Context, bucket, prefix string, duration time.Duration) ([]VolumeLockInfo, error) {
|
|
||||||
logger.LogIf(ctx, NotImplemented{})
|
|
||||||
return []VolumeLockInfo{}, NotImplemented{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearLocks clears namespace locks held in object layer
|
|
||||||
func (a GatewayUnsupported) ClearLocks(ctx context.Context, info []VolumeLockInfo) error {
|
|
||||||
logger.LogIf(ctx, NotImplemented{})
|
|
||||||
return NotImplemented{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshBucketPolicy refreshes cache policy with what's on disk.
|
// RefreshBucketPolicy refreshes cache policy with what's on disk.
|
||||||
func (a GatewayUnsupported) RefreshBucketPolicy(ctx context.Context, bucket string) error {
|
func (a GatewayUnsupported) RefreshBucketPolicy(ctx context.Context, bucket string) error {
|
||||||
logger.LogIf(ctx, NotImplemented{})
|
logger.LogIf(ctx, NotImplemented{})
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ func s3MetaToAzureProperties(ctx context.Context, s3Metadata map[string]string)
|
|||||||
storage.BlobProperties, error) {
|
storage.BlobProperties, error) {
|
||||||
for k := range s3Metadata {
|
for k := range s3Metadata {
|
||||||
if strings.Contains(k, "--") {
|
if strings.Contains(k, "--") {
|
||||||
logger.LogIf(ctx, minio.UnsupportedMetadata{})
|
|
||||||
return storage.BlobMetadata{}, storage.BlobProperties{}, minio.UnsupportedMetadata{}
|
return storage.BlobMetadata{}, storage.BlobProperties{}, minio.UnsupportedMetadata{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,18 +376,12 @@ func getAzureUploadID() (string, error) {
|
|||||||
// checkAzureUploadID - returns error in case of given string is upload ID.
|
// checkAzureUploadID - returns error in case of given string is upload ID.
|
||||||
func checkAzureUploadID(ctx context.Context, uploadID string) (err error) {
|
func checkAzureUploadID(ctx context.Context, uploadID string) (err error) {
|
||||||
if len(uploadID) != 16 {
|
if len(uploadID) != 16 {
|
||||||
logger.LogIf(ctx, minio.MalformedUploadID{
|
|
||||||
UploadID: uploadID,
|
|
||||||
})
|
|
||||||
return minio.MalformedUploadID{
|
return minio.MalformedUploadID{
|
||||||
UploadID: uploadID,
|
UploadID: uploadID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = hex.DecodeString(uploadID); err != nil {
|
if _, err = hex.DecodeString(uploadID); err != nil {
|
||||||
logger.LogIf(ctx, minio.MalformedUploadID{
|
|
||||||
UploadID: uploadID,
|
|
||||||
})
|
|
||||||
return minio.MalformedUploadID{
|
return minio.MalformedUploadID{
|
||||||
UploadID: uploadID,
|
UploadID: uploadID,
|
||||||
}
|
}
|
||||||
@@ -449,7 +442,6 @@ func (a *azureObjects) MakeBucketWithLocation(ctx context.Context, bucket, locat
|
|||||||
// in azure documentation, so we will simply use the same function here.
|
// in azure documentation, so we will simply use the same function here.
|
||||||
// Ref - https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata
|
// Ref - https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata
|
||||||
if !minio.IsValidBucketName(bucket) {
|
if !minio.IsValidBucketName(bucket) {
|
||||||
logger.LogIf(ctx, minio.BucketNameInvalid{Bucket: bucket})
|
|
||||||
return minio.BucketNameInvalid{Bucket: bucket}
|
return minio.BucketNameInvalid{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,7 +449,6 @@ func (a *azureObjects) MakeBucketWithLocation(ctx context.Context, bucket, locat
|
|||||||
err := container.Create(&storage.CreateContainerOptions{
|
err := container.Create(&storage.CreateContainerOptions{
|
||||||
Access: storage.ContainerAccessTypePrivate,
|
Access: storage.ContainerAccessTypePrivate,
|
||||||
})
|
})
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return azureToObjectError(err, bucket)
|
return azureToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +460,6 @@ func (a *azureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi min
|
|||||||
Prefix: bucket,
|
Prefix: bucket,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return bi, azureToObjectError(err, bucket)
|
return bi, azureToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
for _, container := range resp.Containers {
|
for _, container := range resp.Containers {
|
||||||
@@ -483,7 +473,6 @@ func (a *azureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi min
|
|||||||
} // else continue
|
} // else continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.LogIf(ctx, minio.BucketNotFound{Bucket: bucket})
|
|
||||||
return bi, minio.BucketNotFound{Bucket: bucket}
|
return bi, minio.BucketNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,7 +480,6 @@ func (a *azureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi min
|
|||||||
func (a *azureObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketInfo, err error) {
|
func (a *azureObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketInfo, err error) {
|
||||||
resp, err := a.client.ListContainers(storage.ListContainersParameters{})
|
resp, err := a.client.ListContainers(storage.ListContainersParameters{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, azureToObjectError(err)
|
return nil, azureToObjectError(err)
|
||||||
}
|
}
|
||||||
for _, container := range resp.Containers {
|
for _, container := range resp.Containers {
|
||||||
@@ -512,7 +500,6 @@ func (a *azureObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketI
|
|||||||
func (a *azureObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
func (a *azureObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||||
container := a.client.GetContainerReference(bucket)
|
container := a.client.GetContainerReference(bucket)
|
||||||
err := container.Delete(nil)
|
err := container.Delete(nil)
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return azureToObjectError(err, bucket)
|
return azureToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +537,6 @@ func (a *azureObjects) ListObjects(ctx context.Context, bucket, prefix, marker,
|
|||||||
MaxResults: uint(maxKeys),
|
MaxResults: uint(maxKeys),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return result, azureToObjectError(err, bucket, prefix)
|
return result, azureToObjectError(err, bucket, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +597,7 @@ func (a *azureObjects) ListObjects(ctx context.Context, bucket, prefix, marker,
|
|||||||
// ListObjectsV2 - list all blobs in Azure bucket filtered by prefix
|
// ListObjectsV2 - list all blobs in Azure bucket filtered by prefix
|
||||||
func (a *azureObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result minio.ListObjectsV2Info, err error) {
|
func (a *azureObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result minio.ListObjectsV2Info, err error) {
|
||||||
marker := continuationToken
|
marker := continuationToken
|
||||||
if startAfter != "" {
|
if marker == "" {
|
||||||
marker = startAfter
|
marker = startAfter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,7 +624,6 @@ func (a *azureObjects) ListObjectsV2(ctx context.Context, bucket, prefix, contin
|
|||||||
func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
||||||
// startOffset cannot be negative.
|
// startOffset cannot be negative.
|
||||||
if startOffset < 0 {
|
if startOffset < 0 {
|
||||||
logger.LogIf(ctx, minio.InvalidRange{})
|
|
||||||
return azureToObjectError(minio.InvalidRange{}, bucket, object)
|
return azureToObjectError(minio.InvalidRange{}, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +643,6 @@ func (a *azureObjects) GetObject(ctx context.Context, bucket, object string, sta
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return azureToObjectError(err, bucket, object)
|
return azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
_, err = io.Copy(writer, rc)
|
_, err = io.Copy(writer, rc)
|
||||||
@@ -673,7 +657,6 @@ func (a *azureObjects) GetObjectInfo(ctx context.Context, bucket, object string)
|
|||||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||||
err = blob.GetProperties(nil)
|
err = blob.GetProperties(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +682,6 @@ func (a *azureObjects) PutObject(ctx context.Context, bucket, object string, dat
|
|||||||
}
|
}
|
||||||
err = blob.CreateBlockBlobFromReader(data, nil)
|
err = blob.CreateBlockBlobFromReader(data, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
return a.GetObjectInfo(ctx, bucket, object)
|
return a.GetObjectInfo(ctx, bucket, object)
|
||||||
@@ -717,7 +699,6 @@ func (a *azureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, des
|
|||||||
destBlob.Metadata = azureMeta
|
destBlob.Metadata = azureMeta
|
||||||
err = destBlob.Copy(srcBlobURL, nil)
|
err = destBlob.Copy(srcBlobURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
// Azure will copy metadata from the source object when an empty metadata map is provided.
|
// Azure will copy metadata from the source object when an empty metadata map is provided.
|
||||||
@@ -727,14 +708,12 @@ func (a *azureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, des
|
|||||||
destBlob.Metadata = azureMeta
|
destBlob.Metadata = azureMeta
|
||||||
err = destBlob.SetMetadata(nil)
|
err = destBlob.SetMetadata(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
destBlob.Properties = props
|
destBlob.Properties = props
|
||||||
err = destBlob.SetProperties(nil)
|
err = destBlob.SetProperties(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
return objInfo, azureToObjectError(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
return a.GetObjectInfo(ctx, destBucket, destObject)
|
return a.GetObjectInfo(ctx, destBucket, destObject)
|
||||||
@@ -770,14 +749,12 @@ func (a *azureObjects) checkUploadIDExists(ctx context.Context, bucketName, obje
|
|||||||
blob := a.client.GetContainerReference(bucketName).GetBlobReference(
|
blob := a.client.GetContainerReference(bucketName).GetBlobReference(
|
||||||
getAzureMetadataObjectName(objectName, uploadID))
|
getAzureMetadataObjectName(objectName, uploadID))
|
||||||
err = blob.GetMetadata(nil)
|
err = blob.GetMetadata(nil)
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
err = azureToObjectError(err, bucketName, objectName)
|
err = azureToObjectError(err, bucketName, objectName)
|
||||||
oerr := minio.ObjectNotFound{
|
oerr := minio.ObjectNotFound{
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Object: objectName,
|
Object: objectName,
|
||||||
}
|
}
|
||||||
if err == oerr {
|
if err == oerr {
|
||||||
logger.LogIf(ctx, minio.InvalidUploadID{UploadID: uploadID})
|
|
||||||
err = minio.InvalidUploadID{
|
err = minio.InvalidUploadID{
|
||||||
UploadID: uploadID,
|
UploadID: uploadID,
|
||||||
}
|
}
|
||||||
@@ -803,7 +780,6 @@ func (a *azureObjects) NewMultipartUpload(ctx context.Context, bucket, object st
|
|||||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
|
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
|
||||||
err = blob.CreateBlockBlobFromReader(bytes.NewBuffer(jsonData), nil)
|
err = blob.CreateBlockBlobFromReader(bytes.NewBuffer(jsonData), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return "", azureToObjectError(err, bucket, metadataObject)
|
return "", azureToObjectError(err, bucket, metadataObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -840,7 +816,6 @@ func (a *azureObjects) PutObjectPart(ctx context.Context, bucket, object, upload
|
|||||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
blob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||||
err = blob.PutBlockWithLength(id, uint64(subPartSize), io.LimitReader(data, subPartSize), nil)
|
err = blob.PutBlockWithLength(id, uint64(subPartSize), io.LimitReader(data, subPartSize), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return info, azureToObjectError(err, bucket, object)
|
return info, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
subPartNumber++
|
subPartNumber++
|
||||||
@@ -872,7 +847,6 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return result, azureToObjectError(err, bucket, object)
|
return result, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
// Build a sorted list of parts and return the requested entries.
|
// Build a sorted list of parts and return the requested entries.
|
||||||
@@ -882,7 +856,6 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
|
|||||||
var parsedUploadID string
|
var parsedUploadID string
|
||||||
var md5Hex string
|
var md5Hex string
|
||||||
if partNumber, _, parsedUploadID, md5Hex, err = azureParseBlockID(block.Name); err != nil {
|
if partNumber, _, parsedUploadID, md5Hex, err = azureParseBlockID(block.Name); err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unexpected error"))
|
|
||||||
return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object)
|
return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object)
|
||||||
}
|
}
|
||||||
if parsedUploadID != uploadID {
|
if parsedUploadID != uploadID {
|
||||||
@@ -900,7 +873,6 @@ func (a *azureObjects) ListObjectParts(ctx context.Context, bucket, object, uplo
|
|||||||
if part.ETag != md5Hex {
|
if part.ETag != md5Hex {
|
||||||
// If two parts of same partNumber were uploaded with different contents
|
// If two parts of same partNumber were uploaded with different contents
|
||||||
// return error as we won't be able to decide which the latest part is.
|
// return error as we won't be able to decide which the latest part is.
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unexpected error"))
|
|
||||||
return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object)
|
return result, azureToObjectError(fmt.Errorf("Unexpected error"), bucket, object)
|
||||||
}
|
}
|
||||||
part.Size += block.Size
|
part.Size += block.Size
|
||||||
@@ -967,7 +939,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
|||||||
var metadataReader io.Reader
|
var metadataReader io.Reader
|
||||||
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
|
blob := a.client.GetContainerReference(bucket).GetBlobReference(metadataObject)
|
||||||
if metadataReader, err = blob.Get(nil); err != nil {
|
if metadataReader, err = blob.Get(nil); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, bucket, metadataObject)
|
return objInfo, azureToObjectError(err, bucket, metadataObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -991,7 +962,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
|||||||
objBlob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
objBlob := a.client.GetContainerReference(bucket).GetBlobReference(object)
|
||||||
resp, err := objBlob.GetBlockList(storage.BlockListTypeUncommitted, nil)
|
resp, err := objBlob.GetBlockList(storage.BlockListTypeUncommitted, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,11 +1009,6 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
|||||||
// Error out if parts except last part sizing < 5MiB.
|
// Error out if parts except last part sizing < 5MiB.
|
||||||
for i, size := range partSizes[:len(partSizes)-1] {
|
for i, size := range partSizes[:len(partSizes)-1] {
|
||||||
if size < azureS3MinPartSize {
|
if size < azureS3MinPartSize {
|
||||||
logger.LogIf(ctx, minio.PartTooSmall{
|
|
||||||
PartNumber: uploadedParts[i].PartNumber,
|
|
||||||
PartSize: size,
|
|
||||||
PartETag: uploadedParts[i].ETag,
|
|
||||||
})
|
|
||||||
return objInfo, minio.PartTooSmall{
|
return objInfo, minio.PartTooSmall{
|
||||||
PartNumber: uploadedParts[i].PartNumber,
|
PartNumber: uploadedParts[i].PartNumber,
|
||||||
PartSize: size,
|
PartSize: size,
|
||||||
@@ -1054,23 +1019,19 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
|||||||
|
|
||||||
err = objBlob.PutBlockList(allBlocks, nil)
|
err = objBlob.PutBlockList(allBlocks, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
if len(metadata.Metadata) > 0 {
|
if len(metadata.Metadata) > 0 {
|
||||||
objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(ctx, metadata.Metadata)
|
objBlob.Metadata, objBlob.Properties, err = s3MetaToAzureProperties(ctx, metadata.Metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
err = objBlob.SetProperties(nil)
|
err = objBlob.SetProperties(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
err = objBlob.SetMetadata(nil)
|
err = objBlob.SetMetadata(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, azureToObjectError(err, bucket, object)
|
return objInfo, azureToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1100,15 +1061,12 @@ func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, bucke
|
|||||||
}
|
}
|
||||||
prefix := bucket + "/*" // For all objects inside the bucket.
|
prefix := bucket + "/*" // For all objects inside the bucket.
|
||||||
if len(policies) != 1 {
|
if len(policies) != 1 {
|
||||||
logger.LogIf(ctx, minio.NotImplemented{})
|
|
||||||
return minio.NotImplemented{}
|
return minio.NotImplemented{}
|
||||||
}
|
}
|
||||||
if policies[0].Prefix != prefix {
|
if policies[0].Prefix != prefix {
|
||||||
logger.LogIf(ctx, minio.NotImplemented{})
|
|
||||||
return minio.NotImplemented{}
|
return minio.NotImplemented{}
|
||||||
}
|
}
|
||||||
if policies[0].Policy != miniogopolicy.BucketPolicyReadOnly {
|
if policies[0].Policy != miniogopolicy.BucketPolicyReadOnly {
|
||||||
logger.LogIf(ctx, minio.NotImplemented{})
|
|
||||||
return minio.NotImplemented{}
|
return minio.NotImplemented{}
|
||||||
}
|
}
|
||||||
perm := storage.ContainerPermissions{
|
perm := storage.ContainerPermissions{
|
||||||
@@ -1117,7 +1075,6 @@ func (a *azureObjects) SetBucketPolicy(ctx context.Context, bucket string, bucke
|
|||||||
}
|
}
|
||||||
container := a.client.GetContainerReference(bucket)
|
container := a.client.GetContainerReference(bucket)
|
||||||
err = container.SetPermissions(perm, nil)
|
err = container.SetPermissions(perm, nil)
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return azureToObjectError(err, bucket)
|
return azureToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1126,15 +1083,12 @@ func (a *azureObjects) GetBucketPolicy(ctx context.Context, bucket string) (*pol
|
|||||||
container := a.client.GetContainerReference(bucket)
|
container := a.client.GetContainerReference(bucket)
|
||||||
perm, err := container.GetPermissions(nil)
|
perm, err := container.GetPermissions(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, azureToObjectError(err, bucket)
|
return nil, azureToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
if perm.AccessType == storage.ContainerAccessTypePrivate {
|
if perm.AccessType == storage.ContainerAccessTypePrivate {
|
||||||
logger.LogIf(ctx, minio.BucketPolicyNotFound{Bucket: bucket})
|
|
||||||
return nil, minio.BucketPolicyNotFound{Bucket: bucket}
|
return nil, minio.BucketPolicyNotFound{Bucket: bucket}
|
||||||
} else if perm.AccessType != storage.ContainerAccessTypeContainer {
|
} else if perm.AccessType != storage.ContainerAccessTypeContainer {
|
||||||
logger.LogIf(ctx, minio.NotImplemented{})
|
|
||||||
return nil, azureToObjectError(minio.NotImplemented{})
|
return nil, azureToObjectError(minio.NotImplemented{})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1167,6 +1121,5 @@ func (a *azureObjects) DeleteBucketPolicy(ctx context.Context, bucket string) er
|
|||||||
}
|
}
|
||||||
container := a.client.GetContainerReference(bucket)
|
container := a.client.GetContainerReference(bucket)
|
||||||
err := container.SetPermissions(perm, nil)
|
err := container.SetPermissions(perm, nil)
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return azureToObjectError(err)
|
return azureToObjectError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,7 +278,6 @@ func (l *b2Objects) Bucket(ctx context.Context, bucket string) (*b2.Bucket, erro
|
|||||||
return bkt, nil
|
return bkt, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.LogIf(ctx, minio.BucketNotFound{Bucket: bucket})
|
|
||||||
return nil, minio.BucketNotFound{Bucket: bucket}
|
return nil, minio.BucketNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,12 +354,20 @@ func (l *b2Objects) ListObjects(ctx context.Context, bucket string, prefix strin
|
|||||||
// ListObjectsV2 lists all objects in B2 bucket filtered by prefix, returns upto max 1000 entries at a time.
|
// ListObjectsV2 lists all objects in B2 bucket filtered by prefix, returns upto max 1000 entries at a time.
|
||||||
func (l *b2Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int,
|
func (l *b2Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int,
|
||||||
fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) {
|
fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) {
|
||||||
// fetchOwner, startAfter are not supported and unused.
|
// fetchOwner is not supported and unused.
|
||||||
|
marker := continuationToken
|
||||||
|
if marker == "" {
|
||||||
|
// B2's continuation token is an object name to "start at" rather than "start after"
|
||||||
|
// startAfter plus the lowest character B2 supports is used so that the startAfter
|
||||||
|
// object isn't included in the results
|
||||||
|
marker = startAfter + " "
|
||||||
|
}
|
||||||
|
|
||||||
bkt, err := l.Bucket(ctx, bucket)
|
bkt, err := l.Bucket(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return loi, err
|
return loi, err
|
||||||
}
|
}
|
||||||
files, next, lerr := bkt.ListFileNames(l.ctx, maxKeys, continuationToken, prefix, delimiter)
|
files, next, lerr := bkt.ListFileNames(l.ctx, maxKeys, marker, prefix, delimiter)
|
||||||
if lerr != nil {
|
if lerr != nil {
|
||||||
logger.LogIf(ctx, lerr)
|
logger.LogIf(ctx, lerr)
|
||||||
return loi, b2ToObjectError(lerr, bucket)
|
return loi, b2ToObjectError(lerr, bucket)
|
||||||
@@ -772,7 +779,6 @@ func (l *b2Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy
|
|||||||
// just return back as policy not found for all cases.
|
// just return back as policy not found for all cases.
|
||||||
// CreateBucket always sets the value to allPrivate by default.
|
// CreateBucket always sets the value to allPrivate by default.
|
||||||
if bkt.Type != bucketTypeReadOnly {
|
if bkt.Type != bucketTypeReadOnly {
|
||||||
logger.LogIf(ctx, minio.BucketPolicyNotFound{Bucket: bucket})
|
|
||||||
return nil, minio.BucketPolicyNotFound{Bucket: bucket}
|
return nil, minio.BucketPolicyNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+134
-126
@@ -23,8 +23,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
"math"
|
"math"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -554,16 +554,16 @@ func isGCSMarker(marker string) bool {
|
|||||||
|
|
||||||
// ListObjects - lists all blobs in GCS bucket filtered by prefix
|
// ListObjects - lists all blobs in GCS bucket filtered by prefix
|
||||||
func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (minio.ListObjectsInfo, error) {
|
func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (minio.ListObjectsInfo, error) {
|
||||||
|
if maxKeys == 0 {
|
||||||
|
return minio.ListObjectsInfo{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{
|
it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{
|
||||||
Delimiter: delimiter,
|
Delimiter: delimiter,
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
Versions: false,
|
Versions: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
isTruncated := false
|
|
||||||
nextMarker := ""
|
|
||||||
prefixes := []string{}
|
|
||||||
|
|
||||||
// To accommodate S3-compatible applications using
|
// To accommodate S3-compatible applications using
|
||||||
// ListObjectsV1 to use object keys as markers to control the
|
// ListObjectsV1 to use object keys as markers to control the
|
||||||
// listing of objects, we use the following encoding scheme to
|
// listing of objects, we use the following encoding scheme to
|
||||||
@@ -574,83 +574,86 @@ func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix stri
|
|||||||
// prefixing "{minio}" to the GCS continuation token,
|
// prefixing "{minio}" to the GCS continuation token,
|
||||||
// e.g, "{minio}CgRvYmoz"
|
// e.g, "{minio}CgRvYmoz"
|
||||||
//
|
//
|
||||||
// - Application supplied markers are used as-is to list
|
// - Application supplied markers are transformed to a
|
||||||
// object keys that appear after it in the lexicographical order.
|
// GCS continuation token.
|
||||||
|
|
||||||
// If application is using GCS continuation token we should
|
// If application is using GCS continuation token we should
|
||||||
// strip the gcsTokenPrefix we added.
|
// strip the gcsTokenPrefix we added.
|
||||||
gcsMarker := isGCSMarker(marker)
|
token := ""
|
||||||
if gcsMarker {
|
if marker != "" {
|
||||||
it.PageInfo().Token = strings.TrimPrefix(marker, gcsTokenPrefix)
|
if isGCSMarker(marker) {
|
||||||
|
token = strings.TrimPrefix(marker, gcsTokenPrefix)
|
||||||
|
} else {
|
||||||
|
token = toGCSPageToken(marker)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
nextMarker := ""
|
||||||
|
|
||||||
it.PageInfo().MaxSize = maxKeys
|
var prefixes []string
|
||||||
|
var objects []minio.ObjectInfo
|
||||||
|
var nextPageToken string
|
||||||
|
var err error
|
||||||
|
|
||||||
objects := []minio.ObjectInfo{}
|
pager := iterator.NewPager(it, maxKeys, token)
|
||||||
for {
|
for {
|
||||||
if len(objects) >= maxKeys {
|
gcsObjects := make([]*storage.ObjectAttrs, 0)
|
||||||
// check if there is one next object and
|
nextPageToken, err = pager.NextPage(&gcsObjects)
|
||||||
// if that one next object is our hidden
|
|
||||||
// metadata folder, then just break
|
|
||||||
// otherwise we've truncated the output
|
|
||||||
attrs, _ := it.Next()
|
|
||||||
if attrs != nil && attrs.Prefix == minio.GatewayMinioSysTmp {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
isTruncated = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs, err := it.Next()
|
|
||||||
if err == iterator.Done {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return minio.ListObjectsInfo{}, gcsToObjectError(err, bucket, prefix)
|
return minio.ListObjectsInfo{}, gcsToObjectError(err, bucket, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
nextMarker = toGCSPageToken(attrs.Name)
|
for _, attrs := range gcsObjects {
|
||||||
|
|
||||||
if attrs.Prefix == minio.GatewayMinioSysTmp {
|
// Due to minio.GatewayMinioSysTmp keys being skipped, the number of objects + prefixes
|
||||||
// We don't return our metadata prefix.
|
// returned may not total maxKeys. This behavior is compatible with the S3 spec which
|
||||||
continue
|
// allows the response to include less keys than maxKeys.
|
||||||
}
|
if attrs.Prefix == minio.GatewayMinioSysTmp {
|
||||||
if !strings.HasPrefix(prefix, minio.GatewayMinioSysTmp) {
|
// We don't return our metadata prefix.
|
||||||
// If client lists outside gcsMinioPath then we filter out gcsMinioPath/* entries.
|
|
||||||
// But if the client lists inside gcsMinioPath then we return the entries in gcsMinioPath/
|
|
||||||
// which will be helpful to observe the "directory structure" for debugging purposes.
|
|
||||||
if strings.HasPrefix(attrs.Prefix, minio.GatewayMinioSysTmp) ||
|
|
||||||
strings.HasPrefix(attrs.Name, minio.GatewayMinioSysTmp) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
if !strings.HasPrefix(prefix, minio.GatewayMinioSysTmp) {
|
||||||
if attrs.Prefix != "" {
|
// If client lists outside gcsMinioPath then we filter out gcsMinioPath/* entries.
|
||||||
prefixes = append(prefixes, attrs.Prefix)
|
// But if the client lists inside gcsMinioPath then we return the entries in gcsMinioPath/
|
||||||
continue
|
// which will be helpful to observe the "directory structure" for debugging purposes.
|
||||||
}
|
if strings.HasPrefix(attrs.Prefix, minio.GatewayMinioSysTmp) ||
|
||||||
if !gcsMarker && attrs.Name <= marker {
|
strings.HasPrefix(attrs.Name, minio.GatewayMinioSysTmp) {
|
||||||
// if user supplied a marker don't append
|
continue
|
||||||
// objects until we reach marker (and skip it).
|
}
|
||||||
continue
|
}
|
||||||
|
|
||||||
|
if attrs.Prefix != "" {
|
||||||
|
prefixes = append(prefixes, attrs.Prefix)
|
||||||
|
} else {
|
||||||
|
objects = append(objects, fromGCSAttrsToObjectInfo(attrs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The NextMarker property should only be set in the response if a delimiter is used
|
||||||
|
if delimiter != "" {
|
||||||
|
if attrs.Prefix > nextMarker {
|
||||||
|
nextMarker = attrs.Prefix
|
||||||
|
} else if attrs.Name > nextMarker {
|
||||||
|
nextMarker = attrs.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
objects = append(objects, minio.ObjectInfo{
|
// Exit the loop if at least one item can be returned from
|
||||||
Name: attrs.Name,
|
// the current page or there are no more pages available
|
||||||
Bucket: attrs.Bucket,
|
if nextPageToken == "" || len(prefixes)+len(objects) > 0 {
|
||||||
ModTime: attrs.Updated,
|
break
|
||||||
Size: attrs.Size,
|
}
|
||||||
ETag: minio.ToS3ETag(fmt.Sprintf("%d", attrs.CRC32C)),
|
}
|
||||||
UserDefined: attrs.Metadata,
|
|
||||||
ContentType: attrs.ContentType,
|
if nextPageToken == "" {
|
||||||
ContentEncoding: attrs.ContentEncoding,
|
nextMarker = ""
|
||||||
})
|
} else if nextMarker != "" {
|
||||||
|
nextMarker = gcsTokenPrefix + toGCSPageToken(nextMarker)
|
||||||
}
|
}
|
||||||
|
|
||||||
return minio.ListObjectsInfo{
|
return minio.ListObjectsInfo{
|
||||||
IsTruncated: isTruncated,
|
IsTruncated: nextPageToken != "",
|
||||||
NextMarker: gcsTokenPrefix + nextMarker,
|
NextMarker: nextMarker,
|
||||||
Prefixes: prefixes,
|
Prefixes: prefixes,
|
||||||
Objects: objects,
|
Objects: objects,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -658,68 +661,72 @@ func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix stri
|
|||||||
|
|
||||||
// ListObjectsV2 - lists all blobs in GCS bucket filtered by prefix
|
// ListObjectsV2 - lists all blobs in GCS bucket filtered by prefix
|
||||||
func (l *gcsGateway) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (minio.ListObjectsV2Info, error) {
|
func (l *gcsGateway) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (minio.ListObjectsV2Info, error) {
|
||||||
|
if maxKeys == 0 {
|
||||||
|
return minio.ListObjectsV2Info{ContinuationToken: continuationToken}, nil
|
||||||
|
}
|
||||||
|
|
||||||
it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{
|
it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{
|
||||||
Delimiter: delimiter,
|
Delimiter: delimiter,
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
Versions: false,
|
Versions: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
isTruncated := false
|
token := continuationToken
|
||||||
it.PageInfo().MaxSize = maxKeys
|
if token == "" && startAfter != "" {
|
||||||
|
token = toGCSPageToken(startAfter)
|
||||||
if continuationToken != "" {
|
|
||||||
// If client sends continuationToken, set it
|
|
||||||
it.PageInfo().Token = continuationToken
|
|
||||||
} else {
|
|
||||||
// else set the continuationToken to return
|
|
||||||
continuationToken = it.PageInfo().Token
|
|
||||||
if continuationToken != "" {
|
|
||||||
// If GCS SDK sets continuationToken, it means there are more than maxKeys in the current page
|
|
||||||
// and the response will be truncated
|
|
||||||
isTruncated = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var prefixes []string
|
var prefixes []string
|
||||||
var objects []minio.ObjectInfo
|
var objects []minio.ObjectInfo
|
||||||
|
var nextPageToken string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
pager := iterator.NewPager(it, maxKeys, token)
|
||||||
for {
|
for {
|
||||||
attrs, err := it.Next()
|
gcsObjects := make([]*storage.ObjectAttrs, 0)
|
||||||
if err == iterator.Done {
|
nextPageToken, err = pager.NextPage(&gcsObjects)
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return minio.ListObjectsV2Info{}, gcsToObjectError(err, bucket, prefix)
|
return minio.ListObjectsV2Info{}, gcsToObjectError(err, bucket, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
if attrs.Prefix == minio.GatewayMinioSysTmp {
|
for _, attrs := range gcsObjects {
|
||||||
// We don't return our metadata prefix.
|
|
||||||
continue
|
// Due to minio.GatewayMinioSysTmp keys being skipped, the number of objects + prefixes
|
||||||
}
|
// returned may not total maxKeys. This behavior is compatible with the S3 spec which
|
||||||
if !strings.HasPrefix(prefix, minio.GatewayMinioSysTmp) {
|
// allows the response to include less keys than maxKeys.
|
||||||
// If client lists outside gcsMinioPath then we filter out gcsMinioPath/* entries.
|
if attrs.Prefix == minio.GatewayMinioSysTmp {
|
||||||
// But if the client lists inside gcsMinioPath then we return the entries in gcsMinioPath/
|
// We don't return our metadata prefix.
|
||||||
// which will be helpful to observe the "directory structure" for debugging purposes.
|
|
||||||
if strings.HasPrefix(attrs.Prefix, minio.GatewayMinioSysTmp) ||
|
|
||||||
strings.HasPrefix(attrs.Name, minio.GatewayMinioSysTmp) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !strings.HasPrefix(prefix, minio.GatewayMinioSysTmp) {
|
||||||
|
// If client lists outside gcsMinioPath then we filter out gcsMinioPath/* entries.
|
||||||
|
// But if the client lists inside gcsMinioPath then we return the entries in gcsMinioPath/
|
||||||
|
// which will be helpful to observe the "directory structure" for debugging purposes.
|
||||||
|
if strings.HasPrefix(attrs.Prefix, minio.GatewayMinioSysTmp) ||
|
||||||
|
strings.HasPrefix(attrs.Name, minio.GatewayMinioSysTmp) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attrs.Prefix != "" {
|
||||||
|
prefixes = append(prefixes, attrs.Prefix)
|
||||||
|
} else {
|
||||||
|
objects = append(objects, fromGCSAttrsToObjectInfo(attrs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if attrs.Prefix != "" {
|
// Exit the loop if at least one item can be returned from
|
||||||
prefixes = append(prefixes, attrs.Prefix)
|
// the current page or there are no more pages available
|
||||||
continue
|
if nextPageToken == "" || len(prefixes)+len(objects) > 0 {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
objects = append(objects, fromGCSAttrsToObjectInfo(attrs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return minio.ListObjectsV2Info{
|
return minio.ListObjectsV2Info{
|
||||||
IsTruncated: isTruncated,
|
IsTruncated: nextPageToken != "",
|
||||||
ContinuationToken: continuationToken,
|
ContinuationToken: continuationToken,
|
||||||
NextContinuationToken: continuationToken,
|
NextContinuationToken: nextPageToken,
|
||||||
Prefixes: prefixes,
|
Prefixes: prefixes,
|
||||||
Objects: objects,
|
Objects: objects,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -761,22 +768,27 @@ func fromGCSAttrsToObjectInfo(attrs *storage.ObjectAttrs) minio.ObjectInfo {
|
|||||||
// Refer https://cloud.google.com/storage/docs/hashes-etags. Use CRC32C for ETag
|
// Refer https://cloud.google.com/storage/docs/hashes-etags. Use CRC32C for ETag
|
||||||
metadata := make(map[string]string)
|
metadata := make(map[string]string)
|
||||||
for k, v := range attrs.Metadata {
|
for k, v := range attrs.Metadata {
|
||||||
|
k = http.CanonicalHeaderKey(k)
|
||||||
|
// Translate the GCS custom metadata prefix
|
||||||
|
if strings.HasPrefix(k, "X-Goog-Meta-") {
|
||||||
|
k = strings.Replace(k, "X-Goog-Meta-", "X-Amz-Meta-", 1)
|
||||||
|
}
|
||||||
metadata[k] = v
|
metadata[k] = v
|
||||||
}
|
}
|
||||||
if attrs.ContentType != "" {
|
if attrs.ContentType != "" {
|
||||||
metadata["content-type"] = attrs.ContentType
|
metadata["Content-Type"] = attrs.ContentType
|
||||||
}
|
}
|
||||||
if attrs.ContentEncoding != "" {
|
if attrs.ContentEncoding != "" {
|
||||||
metadata["content-encoding"] = attrs.ContentEncoding
|
metadata["Content-Encoding"] = attrs.ContentEncoding
|
||||||
}
|
}
|
||||||
if attrs.CacheControl != "" {
|
if attrs.CacheControl != "" {
|
||||||
metadata["cache-control"] = attrs.CacheControl
|
metadata["Cache-Control"] = attrs.CacheControl
|
||||||
}
|
}
|
||||||
if attrs.ContentDisposition != "" {
|
if attrs.ContentDisposition != "" {
|
||||||
metadata["content-disposition"] = attrs.ContentDisposition
|
metadata["Content-Disposition"] = attrs.ContentDisposition
|
||||||
}
|
}
|
||||||
if attrs.ContentLanguage != "" {
|
if attrs.ContentLanguage != "" {
|
||||||
metadata["content-language"] = attrs.ContentLanguage
|
metadata["Content-Language"] = attrs.ContentLanguage
|
||||||
}
|
}
|
||||||
return minio.ObjectInfo{
|
return minio.ObjectInfo{
|
||||||
Name: attrs.Name,
|
Name: attrs.Name,
|
||||||
@@ -792,21 +804,25 @@ func fromGCSAttrsToObjectInfo(attrs *storage.ObjectAttrs) minio.ObjectInfo {
|
|||||||
|
|
||||||
// applyMetadataToGCSAttrs applies metadata to a GCS ObjectAttrs instance
|
// applyMetadataToGCSAttrs applies metadata to a GCS ObjectAttrs instance
|
||||||
func applyMetadataToGCSAttrs(metadata map[string]string, attrs *storage.ObjectAttrs) {
|
func applyMetadataToGCSAttrs(metadata map[string]string, attrs *storage.ObjectAttrs) {
|
||||||
attrs.ContentType = metadata["content-type"]
|
|
||||||
attrs.ContentEncoding = metadata["content-encoding"]
|
|
||||||
attrs.CacheControl = metadata["cache-control"]
|
|
||||||
attrs.ContentDisposition = metadata["content-disposition"]
|
|
||||||
attrs.ContentLanguage = metadata["content-language"]
|
|
||||||
|
|
||||||
attrs.Metadata = make(map[string]string)
|
attrs.Metadata = make(map[string]string)
|
||||||
for k, v := range metadata {
|
for k, v := range metadata {
|
||||||
attrs.Metadata[k] = v
|
k = http.CanonicalHeaderKey(k)
|
||||||
}
|
switch {
|
||||||
// Filter metadata which is stored as a unique attribute
|
case strings.HasPrefix(k, "X-Amz-Meta-"):
|
||||||
for _, key := range []string{
|
// Translate the S3 user-defined metadata prefix
|
||||||
"content-type", "content-encoding", "cache-control", "content-disposition", "content-language",
|
k = strings.Replace(k, "X-Amz-Meta-", "x-goog-meta-", 1)
|
||||||
} {
|
attrs.Metadata[k] = v
|
||||||
delete(attrs.Metadata, key)
|
case k == "Content-Type":
|
||||||
|
attrs.ContentType = v
|
||||||
|
case k == "Content-Encoding":
|
||||||
|
attrs.ContentEncoding = v
|
||||||
|
case k == "Cache-Control":
|
||||||
|
attrs.CacheControl = v
|
||||||
|
case k == "Content-Disposition":
|
||||||
|
attrs.ContentDisposition = v
|
||||||
|
case k == "Content-Language":
|
||||||
|
attrs.ContentLanguage = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,12 +1027,8 @@ func (l *gcsGateway) AbortMultipartUpload(ctx context.Context, bucket string, ke
|
|||||||
|
|
||||||
// CompleteMultipartUpload completes ongoing multipart upload and finalizes object
|
// CompleteMultipartUpload completes ongoing multipart upload and finalizes object
|
||||||
// Note that there is a limit (currently 32) to the number of components that can
|
// Note that there is a limit (currently 32) to the number of components that can
|
||||||
// be composed in a single operation. There is a limit (currently 1024) to the total
|
// be composed in a single operation. There is a per-project rate limit (currently 200)
|
||||||
// number of components for a given composite object. This means you can append to
|
// to the number of source objects you can compose per second.
|
||||||
// each object at most 1023 times. There is a per-project rate limit (currently 200)
|
|
||||||
// to the number of components you can compose per second. This rate counts both the
|
|
||||||
// components being appended to a composite object as well as the components being
|
|
||||||
// copied when the composite object of which they are a part is copied.
|
|
||||||
func (l *gcsGateway) CompleteMultipartUpload(ctx context.Context, bucket string, key string, uploadID string, uploadedParts []minio.CompletePart) (minio.ObjectInfo, error) {
|
func (l *gcsGateway) CompleteMultipartUpload(ctx context.Context, bucket string, key string, uploadID string, uploadedParts []minio.CompletePart) (minio.ObjectInfo, error) {
|
||||||
meta := gcsMultipartMetaName(uploadID)
|
meta := gcsMultipartMetaName(uploadID)
|
||||||
object := l.client.Bucket(bucket).Object(meta)
|
object := l.client.Bucket(bucket).Object(meta)
|
||||||
@@ -1136,7 +1148,6 @@ func (l *gcsGateway) CompleteMultipartUpload(ctx context.Context, bucket string,
|
|||||||
func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
|
func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
|
||||||
policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy)
|
policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This should not happen.
|
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return gcsToObjectError(err, bucket)
|
return gcsToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
@@ -1192,7 +1203,6 @@ func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, bucketP
|
|||||||
func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
|
func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
|
||||||
rules, err := l.client.Bucket(bucket).ACL().List(l.ctx)
|
rules, err := l.client.Bucket(bucket).ACL().List(l.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, gcsToObjectError(err, bucket)
|
return nil, gcsToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,7 +1237,6 @@ func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*polic
|
|||||||
|
|
||||||
// Return NoSuchBucketPolicy error, when policy is not set
|
// Return NoSuchBucketPolicy error, when policy is not set
|
||||||
if len(actionSet) == 0 {
|
if len(actionSet) == 0 {
|
||||||
logger.LogIf(ctx, minio.BucketPolicyNotFound{})
|
|
||||||
return nil, gcsToObjectError(minio.BucketPolicyNotFound{}, bucket)
|
return nil, gcsToObjectError(minio.BucketPolicyNotFound{}, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1252,7 +1261,6 @@ func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*polic
|
|||||||
func (l *gcsGateway) DeleteBucketPolicy(ctx context.Context, bucket string) error {
|
func (l *gcsGateway) DeleteBucketPolicy(ctx context.Context, bucket string) error {
|
||||||
// This only removes the storage.AllUsers policies
|
// This only removes the storage.AllUsers policies
|
||||||
if err := l.client.Bucket(bucket).ACL().Delete(l.ctx, storage.AllUsers); err != nil {
|
if err := l.client.Bucket(bucket).ACL().Delete(l.ctx, storage.AllUsers); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return gcsToObjectError(err, bucket)
|
return gcsToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cloud.google.com/go/storage"
|
||||||
"google.golang.org/api/googleapi"
|
"google.golang.org/api/googleapi"
|
||||||
|
|
||||||
miniogo "github.com/minio/minio-go"
|
miniogo "github.com/minio/minio-go"
|
||||||
@@ -393,3 +395,105 @@ func TestGCSToObjectError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestS3MetaToGCSAttributes(t *testing.T) {
|
||||||
|
headers := map[string]string{
|
||||||
|
"accept-encoding": "gzip",
|
||||||
|
"content-encoding": "gzip",
|
||||||
|
"cache-control": "age: 3600",
|
||||||
|
"content-disposition": "dummy",
|
||||||
|
"content-type": "application/javascript",
|
||||||
|
"Content-Language": "en",
|
||||||
|
"X-Amz-Meta-Hdr": "value",
|
||||||
|
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||||
|
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
||||||
|
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||||
|
}
|
||||||
|
// Only X-Amz-Meta- prefixed entries will be returned in
|
||||||
|
// Metadata (without the prefix!)
|
||||||
|
expectedHeaders := map[string]string{
|
||||||
|
"x-goog-meta-Hdr": "value",
|
||||||
|
"x-goog-meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||||
|
"x-goog-meta-X-Amz-Matdesc": "{}",
|
||||||
|
"x-goog-meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := storage.ObjectAttrs{}
|
||||||
|
applyMetadataToGCSAttrs(headers, &attrs)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(attrs.Metadata, expectedHeaders) {
|
||||||
|
t.Fatalf("Test failed, expected %#v, got %#v", expectedHeaders, attrs.Metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attrs.CacheControl != headers["cache-control"] {
|
||||||
|
t.Fatalf("Test failed with Cache-Control mistmatch, expected %s, got %s", headers["cache-control"], attrs.CacheControl)
|
||||||
|
}
|
||||||
|
if attrs.ContentDisposition != headers["content-disposition"] {
|
||||||
|
t.Fatalf("Test failed with Content-Disposition mistmatch, expected %s, got %s", headers["content-disposition"], attrs.ContentDisposition)
|
||||||
|
}
|
||||||
|
if attrs.ContentEncoding != headers["content-encoding"] {
|
||||||
|
t.Fatalf("Test failed with Content-Encoding mistmatch, expected %s, got %s", headers["content-encoding"], attrs.ContentEncoding)
|
||||||
|
}
|
||||||
|
if attrs.ContentLanguage != headers["Content-Language"] {
|
||||||
|
t.Fatalf("Test failed with Content-Language mistmatch, expected %s, got %s", headers["Content-Language"], attrs.ContentLanguage)
|
||||||
|
}
|
||||||
|
if attrs.ContentType != headers["content-type"] {
|
||||||
|
t.Fatalf("Test failed with Content-Type mistmatch, expected %s, got %s", headers["content-type"], attrs.ContentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGCSAttrsToObjectInfo(t *testing.T) {
|
||||||
|
metadata := map[string]string{
|
||||||
|
"x-goog-meta-Hdr": "value",
|
||||||
|
"x-goog-meta-x_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||||
|
"x-goog-meta-x-amz-matdesc": "{}",
|
||||||
|
"x-goog-meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||||
|
}
|
||||||
|
expectedMeta := map[string]string{
|
||||||
|
"X-Amz-Meta-Hdr": "value",
|
||||||
|
"X-Amz-Meta-X_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||||
|
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
||||||
|
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||||
|
"Cache-Control": "max-age: 3600",
|
||||||
|
"Content-Disposition": "dummy",
|
||||||
|
"Content-Encoding": "gzip",
|
||||||
|
"Content-Language": "en",
|
||||||
|
"Content-Type": "application/javascript",
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := storage.ObjectAttrs{
|
||||||
|
Name: "test-obj",
|
||||||
|
Bucket: "test-bucket",
|
||||||
|
Updated: time.Now(),
|
||||||
|
Size: 123,
|
||||||
|
CRC32C: 45312398,
|
||||||
|
CacheControl: "max-age: 3600",
|
||||||
|
ContentDisposition: "dummy",
|
||||||
|
ContentEncoding: "gzip",
|
||||||
|
ContentLanguage: "en",
|
||||||
|
ContentType: "application/javascript",
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
expectedETag := minio.ToS3ETag(fmt.Sprintf("%d", attrs.CRC32C))
|
||||||
|
|
||||||
|
objInfo := fromGCSAttrsToObjectInfo(&attrs)
|
||||||
|
if !reflect.DeepEqual(objInfo.UserDefined, expectedMeta) {
|
||||||
|
t.Fatalf("Test failed, expected %#v, got %#v", expectedMeta, objInfo.UserDefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
if objInfo.Name != attrs.Name {
|
||||||
|
t.Fatalf("Test failed with Name mistmatch, expected %s, got %s", attrs.Name, objInfo.Name)
|
||||||
|
}
|
||||||
|
if objInfo.Bucket != attrs.Bucket {
|
||||||
|
t.Fatalf("Test failed with Bucket mistmatch, expected %s, got %s", attrs.Bucket, objInfo.Bucket)
|
||||||
|
}
|
||||||
|
if objInfo.ModTime != attrs.Updated {
|
||||||
|
t.Fatalf("Test failed with ModTime mistmatch, expected %s, got %s", attrs.Updated, objInfo.ModTime)
|
||||||
|
}
|
||||||
|
if objInfo.Size != attrs.Size {
|
||||||
|
t.Fatalf("Test failed with Size mistmatch, expected %d, got %d", attrs.Size, objInfo.Size)
|
||||||
|
}
|
||||||
|
if objInfo.ETag != expectedETag {
|
||||||
|
t.Fatalf("Test failed with ETag mistmatch, expected %s, got %s", expectedETag, objInfo.ETag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -347,6 +347,13 @@ func (t *tritonObjects) ListObjects(ctx context.Context, bucket, prefix, marker,
|
|||||||
dirName = path.Join(mantaRoot, bucket, pathDir)
|
dirName = path.Join(mantaRoot, bucket, pathDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if marker != "" {
|
||||||
|
// Manta uses the marker as the key to start at rather than start after
|
||||||
|
// A space is appended to the marker so that the corresponding object is not
|
||||||
|
// included in the results
|
||||||
|
marker += " "
|
||||||
|
}
|
||||||
|
|
||||||
input = &storage.ListDirectoryInput{
|
input = &storage.ListDirectoryInput{
|
||||||
DirectoryName: dirName,
|
DirectoryName: dirName,
|
||||||
Limit: uint64(maxKeys),
|
Limit: uint64(maxKeys),
|
||||||
@@ -419,6 +426,18 @@ func (t *tritonObjects) ListObjectsV2(ctx context.Context, bucket, prefix, conti
|
|||||||
pathBase = path.Base(prefix)
|
pathBase = path.Base(prefix)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
marker := continuationToken
|
||||||
|
if marker == "" {
|
||||||
|
marker = startAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
if marker != "" {
|
||||||
|
// Manta uses the marker as the key to start at rather than start after.
|
||||||
|
// A space is appended to the marker so that the corresponding object is not
|
||||||
|
// included in the results
|
||||||
|
marker += " "
|
||||||
|
}
|
||||||
|
|
||||||
if pathDir := path.Dir(prefix); pathDir == "." {
|
if pathDir := path.Dir(prefix); pathDir == "." {
|
||||||
dirName = path.Join(mantaRoot, bucket)
|
dirName = path.Join(mantaRoot, bucket)
|
||||||
} else {
|
} else {
|
||||||
@@ -428,7 +447,7 @@ func (t *tritonObjects) ListObjectsV2(ctx context.Context, bucket, prefix, conti
|
|||||||
input = &storage.ListDirectoryInput{
|
input = &storage.ListDirectoryInput{
|
||||||
DirectoryName: dirName,
|
DirectoryName: dirName,
|
||||||
Limit: uint64(maxKeys),
|
Limit: uint64(maxKeys),
|
||||||
Marker: continuationToken,
|
Marker: marker,
|
||||||
}
|
}
|
||||||
objs, err = t.client.Dir().List(ctx, input)
|
objs, err = t.client.Dir().List(ctx, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -479,8 +479,11 @@ func ossListObjects(ctx context.Context, client *oss.Client, bucket, prefix, mar
|
|||||||
// ossListObjectsV2 lists all blobs in OSS bucket filtered by prefix.
|
// ossListObjectsV2 lists all blobs in OSS bucket filtered by prefix.
|
||||||
func ossListObjectsV2(ctx context.Context, client *oss.Client, bucket, prefix, continuationToken, delimiter string, maxKeys int,
|
func ossListObjectsV2(ctx context.Context, client *oss.Client, bucket, prefix, continuationToken, delimiter string, maxKeys int,
|
||||||
fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) {
|
fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, err error) {
|
||||||
// fetchOwner and startAfter are not supported and unused.
|
// fetchOwner is not supported and unused.
|
||||||
marker := continuationToken
|
marker := continuationToken
|
||||||
|
if marker == "" {
|
||||||
|
marker = startAfter
|
||||||
|
}
|
||||||
|
|
||||||
resultV1, err := ossListObjects(ctx, client, bucket, prefix, marker, delimiter, maxKeys)
|
resultV1, err := ossListObjects(ctx, client, bucket, prefix, marker, delimiter, maxKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1018,7 +1021,6 @@ func (l *ossObjects) GetBucketPolicy(ctx context.Context, bucket string) (*polic
|
|||||||
switch result.ACL {
|
switch result.ACL {
|
||||||
case string(oss.ACLPrivate):
|
case string(oss.ACLPrivate):
|
||||||
// By default, all buckets starts with a "private" policy.
|
// By default, all buckets starts with a "private" policy.
|
||||||
logger.LogIf(ctx, minio.BucketPolicyNotFound{})
|
|
||||||
return nil, ossToObjectError(minio.BucketPolicyNotFound{}, bucket)
|
return nil, ossToObjectError(minio.BucketPolicyNotFound{}, bucket)
|
||||||
case string(oss.ACLPublicRead):
|
case string(oss.ACLPublicRead):
|
||||||
readOnly = true
|
readOnly = true
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/minio/cli"
|
"github.com/minio/cli"
|
||||||
miniogo "github.com/minio/minio-go"
|
miniogo "github.com/minio/minio-go"
|
||||||
@@ -121,6 +123,31 @@ func (g *S3) Name() string {
|
|||||||
return s3Backend
|
return s3Backend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyz01234569"
|
||||||
|
const (
|
||||||
|
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||||
|
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||||
|
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||||
|
)
|
||||||
|
|
||||||
|
// randString generates random names and prepends them with a known prefix.
|
||||||
|
func randString(n int, src rand.Source, prefix string) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
// A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
|
||||||
|
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
||||||
|
if remain == 0 {
|
||||||
|
cache, remain = src.Int63(), letterIdxMax
|
||||||
|
}
|
||||||
|
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||||
|
b[i] = letterBytes[idx]
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
cache >>= letterIdxBits
|
||||||
|
remain--
|
||||||
|
}
|
||||||
|
return prefix + string(b[0:30-len(prefix)])
|
||||||
|
}
|
||||||
|
|
||||||
// newS3 - Initializes a new client by auto probing S3 server signature.
|
// newS3 - Initializes a new client by auto probing S3 server signature.
|
||||||
func newS3(url, accessKey, secretKey string) (*miniogo.Core, error) {
|
func newS3(url, accessKey, secretKey string) (*miniogo.Core, error) {
|
||||||
if url == "" {
|
if url == "" {
|
||||||
@@ -137,13 +164,13 @@ func newS3(url, accessKey, secretKey string) (*miniogo.Core, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
probeBucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "probe-bucket-sign-")
|
||||||
if _, err = clnt.BucketExists("probe-bucket-sign"); err != nil {
|
if _, err = clnt.BucketExists(probeBucketName); err != nil {
|
||||||
clnt, err = miniogo.NewV2(endpoint, accessKey, secretKey, secure)
|
clnt, err = miniogo.NewV2(endpoint, accessKey, secretKey, secure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err = clnt.BucketExists("probe-bucket-sign"); err != nil {
|
if _, err = clnt.BucketExists(probeBucketName); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,13 +223,11 @@ func (l *s3Objects) MakeBucketWithLocation(ctx context.Context, bucket, location
|
|||||||
// access to these buckets.
|
// access to these buckets.
|
||||||
// Ref - http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
|
// Ref - http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
|
||||||
if s3utils.CheckValidBucketName(bucket) != nil {
|
if s3utils.CheckValidBucketName(bucket) != nil {
|
||||||
logger.LogIf(ctx, minio.BucketNameInvalid{Bucket: bucket})
|
|
||||||
return minio.BucketNameInvalid{Bucket: bucket}
|
return minio.BucketNameInvalid{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := l.Client.MakeBucket(bucket, location)
|
err := l.Client.MakeBucket(bucket, location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return minio.ErrorRespToObjectError(err, bucket)
|
return minio.ErrorRespToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -212,7 +237,6 @@ func (l *s3Objects) MakeBucketWithLocation(ctx context.Context, bucket, location
|
|||||||
func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, e error) {
|
func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.BucketInfo, e error) {
|
||||||
buckets, err := l.Client.ListBuckets()
|
buckets, err := l.Client.ListBuckets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return bi, minio.ErrorRespToObjectError(err, bucket)
|
return bi, minio.ErrorRespToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +251,6 @@ func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogIf(ctx, minio.BucketNotFound{Bucket: bucket})
|
|
||||||
return bi, minio.BucketNotFound{Bucket: bucket}
|
return bi, minio.BucketNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +258,6 @@ func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.
|
|||||||
func (l *s3Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error) {
|
func (l *s3Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error) {
|
||||||
buckets, err := l.Client.ListBuckets()
|
buckets, err := l.Client.ListBuckets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, minio.ErrorRespToObjectError(err)
|
return nil, minio.ErrorRespToObjectError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +276,6 @@ func (l *s3Objects) ListBuckets(ctx context.Context) ([]minio.BucketInfo, error)
|
|||||||
func (l *s3Objects) DeleteBucket(ctx context.Context, bucket string) error {
|
func (l *s3Objects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||||
err := l.Client.RemoveBucket(bucket)
|
err := l.Client.RemoveBucket(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return minio.ErrorRespToObjectError(err, bucket)
|
return minio.ErrorRespToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -264,7 +285,6 @@ func (l *s3Objects) DeleteBucket(ctx context.Context, bucket string) error {
|
|||||||
func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, e error) {
|
func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, e error) {
|
||||||
result, err := l.Client.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
result, err := l.Client.ListObjects(bucket, prefix, marker, delimiter, maxKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return loi, minio.ErrorRespToObjectError(err, bucket)
|
return loi, minio.ErrorRespToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +295,6 @@ func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix strin
|
|||||||
func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) {
|
func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) {
|
||||||
result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys, startAfter)
|
result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys, startAfter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return loi, minio.ErrorRespToObjectError(err, bucket)
|
return loi, minio.ErrorRespToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +309,6 @@ func (l *s3Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuat
|
|||||||
// length indicates the total length of the object.
|
// length indicates the total length of the object.
|
||||||
func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string) error {
|
||||||
if length < 0 && length != -1 {
|
if length < 0 && length != -1 {
|
||||||
logger.LogIf(ctx, minio.InvalidRange{})
|
|
||||||
return minio.ErrorRespToObjectError(minio.InvalidRange{}, bucket, key)
|
return minio.ErrorRespToObjectError(minio.InvalidRange{}, bucket, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +321,6 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
|
|||||||
}
|
}
|
||||||
object, _, err := l.Client.GetObject(bucket, key, opts)
|
object, _, err := l.Client.GetObject(bucket, key, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return minio.ErrorRespToObjectError(err, bucket, key)
|
return minio.ErrorRespToObjectError(err, bucket, key)
|
||||||
}
|
}
|
||||||
defer object.Close()
|
defer object.Close()
|
||||||
@@ -319,7 +336,6 @@ func (l *s3Objects) GetObject(ctx context.Context, bucket string, key string, st
|
|||||||
func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object string) (objInfo minio.ObjectInfo, err error) {
|
func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object string) (objInfo minio.ObjectInfo, err error) {
|
||||||
oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{})
|
oi, err := l.Client.StatObject(bucket, object, miniogo.StatObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return minio.ObjectInfo{}, minio.ErrorRespToObjectError(err, bucket, object)
|
return minio.ObjectInfo{}, minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +346,6 @@ func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object str
|
|||||||
func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
|
func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, data *hash.Reader, metadata map[string]string) (objInfo minio.ObjectInfo, err error) {
|
||||||
oi, err := l.Client.PutObject(bucket, object, data, data.Size(), data.MD5Base64String(), data.SHA256HexString(), minio.ToMinioClientMetadata(metadata))
|
oi, err := l.Client.PutObject(bucket, object, data, data.Size(), data.MD5Base64String(), data.SHA256HexString(), minio.ToMinioClientMetadata(metadata))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, minio.ErrorRespToObjectError(err, bucket, object)
|
return objInfo, minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +361,6 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
|
|||||||
srcInfo.UserDefined["x-amz-metadata-directive"] = "REPLACE"
|
srcInfo.UserDefined["x-amz-metadata-directive"] = "REPLACE"
|
||||||
srcInfo.UserDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
srcInfo.UserDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
||||||
if _, err = l.Client.CopyObject(srcBucket, srcObject, dstBucket, dstObject, srcInfo.UserDefined); err != nil {
|
if _, err = l.Client.CopyObject(srcBucket, srcObject, dstBucket, dstObject, srcInfo.UserDefined); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
return l.GetObjectInfo(ctx, dstBucket, dstObject)
|
return l.GetObjectInfo(ctx, dstBucket, dstObject)
|
||||||
@@ -356,7 +370,6 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
|
|||||||
func (l *s3Objects) DeleteObject(ctx context.Context, bucket string, object string) error {
|
func (l *s3Objects) DeleteObject(ctx context.Context, bucket string, object string) error {
|
||||||
err := l.Client.RemoveObject(bucket, object)
|
err := l.Client.RemoveObject(bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return minio.ErrorRespToObjectError(err, bucket, object)
|
return minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,7 +392,6 @@ func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, objec
|
|||||||
opts := miniogo.PutObjectOptions{UserMetadata: metadata}
|
opts := miniogo.PutObjectOptions{UserMetadata: metadata}
|
||||||
uploadID, err = l.Client.NewMultipartUpload(bucket, object, opts)
|
uploadID, err = l.Client.NewMultipartUpload(bucket, object, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return uploadID, minio.ErrorRespToObjectError(err, bucket, object)
|
return uploadID, minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
return uploadID, nil
|
return uploadID, nil
|
||||||
@@ -389,7 +401,6 @@ func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, objec
|
|||||||
func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *hash.Reader) (pi minio.PartInfo, e error) {
|
func (l *s3Objects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *hash.Reader) (pi minio.PartInfo, e error) {
|
||||||
info, err := l.Client.PutObjectPart(bucket, object, uploadID, partID, data, data.Size(), data.MD5Base64String(), data.SHA256HexString())
|
info, err := l.Client.PutObjectPart(bucket, object, uploadID, partID, data, data.Size(), data.MD5Base64String(), data.SHA256HexString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return pi, minio.ErrorRespToObjectError(err, bucket, object)
|
return pi, minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +418,6 @@ func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, de
|
|||||||
completePart, err := l.Client.CopyObjectPart(srcBucket, srcObject, destBucket, destObject,
|
completePart, err := l.Client.CopyObjectPart(srcBucket, srcObject, destBucket, destObject,
|
||||||
uploadID, partID, startOffset, length, srcInfo.UserDefined)
|
uploadID, partID, startOffset, length, srcInfo.UserDefined)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return p, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
return p, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||||
}
|
}
|
||||||
p.PartNumber = completePart.PartNumber
|
p.PartNumber = completePart.PartNumber
|
||||||
@@ -428,7 +438,6 @@ func (l *s3Objects) ListObjectParts(ctx context.Context, bucket string, object s
|
|||||||
// AbortMultipartUpload aborts a ongoing multipart upload
|
// AbortMultipartUpload aborts a ongoing multipart upload
|
||||||
func (l *s3Objects) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string) error {
|
func (l *s3Objects) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string) error {
|
||||||
err := l.Client.AbortMultipartUpload(bucket, object, uploadID)
|
err := l.Client.AbortMultipartUpload(bucket, object, uploadID)
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return minio.ErrorRespToObjectError(err, bucket, object)
|
return minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +445,6 @@ func (l *s3Objects) AbortMultipartUpload(ctx context.Context, bucket string, obj
|
|||||||
func (l *s3Objects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, uploadedParts []minio.CompletePart) (oi minio.ObjectInfo, e error) {
|
func (l *s3Objects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, uploadedParts []minio.CompletePart) (oi minio.ObjectInfo, e error) {
|
||||||
err := l.Client.CompleteMultipartUpload(bucket, object, uploadID, minio.ToMinioClientCompleteParts(uploadedParts))
|
err := l.Client.CompleteMultipartUpload(bucket, object, uploadID, minio.ToMinioClientCompleteParts(uploadedParts))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return oi, minio.ErrorRespToObjectError(err, bucket, object)
|
return oi, minio.ErrorRespToObjectError(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +461,6 @@ func (l *s3Objects) SetBucketPolicy(ctx context.Context, bucket string, bucketPo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := l.Client.SetBucketPolicy(bucket, string(data)); err != nil {
|
if err := l.Client.SetBucketPolicy(bucket, string(data)); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return minio.ErrorRespToObjectError(err, bucket)
|
return minio.ErrorRespToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +471,6 @@ func (l *s3Objects) SetBucketPolicy(ctx context.Context, bucket string, bucketPo
|
|||||||
func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
|
func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
|
||||||
data, err := l.Client.GetBucketPolicy(bucket)
|
data, err := l.Client.GetBucketPolicy(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return nil, minio.ErrorRespToObjectError(err, bucket)
|
return nil, minio.ErrorRespToObjectError(err, bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +481,6 @@ func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy
|
|||||||
// DeleteBucketPolicy deletes all policies on bucket
|
// DeleteBucketPolicy deletes all policies on bucket
|
||||||
func (l *s3Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error {
|
func (l *s3Objects) DeleteBucketPolicy(ctx context.Context, bucket string) error {
|
||||||
if err := l.Client.SetBucketPolicy(bucket, ""); err != nil {
|
if err := l.Client.SetBucketPolicy(bucket, ""); err != nil {
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
return minio.ErrorRespToObjectError(err, bucket, "")
|
return minio.ErrorRespToObjectError(err, bucket, "")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package sia
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -40,6 +39,7 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
|
"github.com/minio/sha256-simd"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+21
-3
@@ -27,7 +27,6 @@ import (
|
|||||||
|
|
||||||
"github.com/minio/minio-go/pkg/set"
|
"github.com/minio/minio-go/pkg/set"
|
||||||
|
|
||||||
etcd "github.com/coreos/etcd/client"
|
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/dns"
|
"github.com/minio/minio/pkg/dns"
|
||||||
@@ -476,7 +475,6 @@ var notimplementedObjectResourceNames = map[string]bool{
|
|||||||
"policy": true,
|
"policy": true,
|
||||||
"tagging": true,
|
"tagging": true,
|
||||||
"restore": true,
|
"restore": true,
|
||||||
"select": true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resource handler ServeHTTP() wrapper
|
// Resource handler ServeHTTP() wrapper
|
||||||
@@ -653,7 +651,7 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
sr, err := globalDNSConfig.Get(bucket)
|
sr, err := globalDNSConfig.Get(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if etcd.IsKeyNotFound(err) || err == dns.ErrNoEntriesFound {
|
if err == dns.ErrNoEntriesFound {
|
||||||
writeErrorResponse(w, ErrNoSuchBucket, r.URL)
|
writeErrorResponse(w, ErrNoSuchBucket, r.URL)
|
||||||
} else {
|
} else {
|
||||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||||
@@ -724,6 +722,26 @@ func (l rateLimit) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
l.handler.ServeHTTP(w, r)
|
l.handler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requestIDHeaderHandler sets x-amz-request-id header.
|
||||||
|
// Previously, this value was set right before a response
|
||||||
|
// was sent to the client.So, logger and Error response XML
|
||||||
|
// were not using this value.
|
||||||
|
// This is set here so that this header can be logged as
|
||||||
|
// part of the log entry and Error response XML.
|
||||||
|
type requestIDHeaderHandler struct {
|
||||||
|
handler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func addrequestIDHeader(h http.Handler) http.Handler {
|
||||||
|
return requestIDHeaderHandler{handler: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s requestIDHeaderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Set unique request ID for each response.
|
||||||
|
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
|
||||||
|
s.handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
type securityHeaderHandler struct {
|
type securityHeaderHandler struct {
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tests getRedirectLocation function for all its criteria.
|
// Tests getRedirectLocation function for all its criteria.
|
||||||
@@ -153,15 +155,15 @@ var containsReservedMetadataTests = []struct {
|
|||||||
header: http.Header{"X-Minio-Key": []string{"value"}},
|
header: http.Header{"X-Minio-Key": []string{"value"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: http.Header{ServerSideEncryptionIV: []string{"iv"}},
|
header: http.Header{crypto.SSEIV: []string{"iv"}},
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: http.Header{ServerSideEncryptionSealAlgorithm: []string{SSESealAlgorithmDareSha256}},
|
header: http.Header{crypto.SSESealAlgorithm: []string{SSESealAlgorithmDareSha256}},
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: http.Header{ServerSideEncryptionSealedKey: []string{"mac"}},
|
header: http.Header{crypto.SSECSealedKey: []string{"mac"}},
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+80
-8
@@ -18,15 +18,18 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
isatty "github.com/mattn/go-isatty"
|
||||||
"github.com/minio/minio-go/pkg/set"
|
"github.com/minio/minio-go/pkg/set"
|
||||||
|
|
||||||
etcd "github.com/coreos/etcd/client"
|
etcd "github.com/coreos/etcd/clientv3"
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"github.com/minio/minio/cmd/crypto"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/certs"
|
"github.com/minio/minio/pkg/certs"
|
||||||
@@ -124,6 +127,9 @@ var (
|
|||||||
// Holds the host that was passed using --address
|
// Holds the host that was passed using --address
|
||||||
globalMinioHost = ""
|
globalMinioHost = ""
|
||||||
|
|
||||||
|
// globalConfigSys server config system.
|
||||||
|
globalConfigSys *ConfigSys
|
||||||
|
|
||||||
globalNotificationSys *NotificationSys
|
globalNotificationSys *NotificationSys
|
||||||
globalPolicySys *PolicySys
|
globalPolicySys *PolicySys
|
||||||
|
|
||||||
@@ -204,7 +210,7 @@ var (
|
|||||||
globalRPCAPIVersion = RPCVersion{3, 0, 0}
|
globalRPCAPIVersion = RPCVersion{3, 0, 0}
|
||||||
|
|
||||||
// Allocated etcd endpoint for config and bucket DNS.
|
// Allocated etcd endpoint for config and bucket DNS.
|
||||||
globalEtcdClient etcd.Client
|
globalEtcdClient *etcd.Client
|
||||||
|
|
||||||
// Allocated DNS config wrapper over etcd client.
|
// Allocated DNS config wrapper over etcd client.
|
||||||
globalDNSConfig dns.Config
|
globalDNSConfig dns.Config
|
||||||
@@ -214,17 +220,83 @@ var (
|
|||||||
// Usage check interval value.
|
// Usage check interval value.
|
||||||
globalUsageCheckInterval = globalDefaultUsageCheckInterval
|
globalUsageCheckInterval = globalDefaultUsageCheckInterval
|
||||||
|
|
||||||
|
// KMS key id
|
||||||
|
globalKMSKeyID string
|
||||||
|
// Allocated KMS
|
||||||
|
globalKMS crypto.KMS
|
||||||
|
// KMS config
|
||||||
|
globalKMSConfig crypto.KMSConfig
|
||||||
// Add new variable global values here.
|
// Add new variable global values here.
|
||||||
)
|
)
|
||||||
|
|
||||||
// global colors.
|
// global colors.
|
||||||
var (
|
var (
|
||||||
colorBold = color.New(color.Bold).SprintFunc()
|
// Check if we stderr, stdout are dumb terminals, we do not apply
|
||||||
colorRed = color.New(color.FgRed).SprintfFunc()
|
// ansi coloring on dumb terminals.
|
||||||
colorBlue = color.New(color.FgBlue).SprintfFunc()
|
isTerminal = func() bool {
|
||||||
colorYellow = color.New(color.FgYellow).SprintfFunc()
|
return isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stderr.Fd())
|
||||||
colorBgYellow = color.New(color.BgYellow).SprintfFunc()
|
}
|
||||||
colorBlack = color.New(color.FgBlack).SprintfFunc()
|
|
||||||
|
colorBold = func() func(a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
return color.New(color.Bold).SprintFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprint
|
||||||
|
}()
|
||||||
|
colorRed = func() func(format string, a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
return color.New(color.FgRed).SprintfFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf
|
||||||
|
}()
|
||||||
|
colorBlue = func() func(format string, a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
return color.New(color.FgBlue).SprintfFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf
|
||||||
|
}()
|
||||||
|
colorYellow = func() func(format string, a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
return color.New(color.FgYellow).SprintfFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf
|
||||||
|
}()
|
||||||
|
colorCyanBold = func() func(a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
color.New(color.FgCyan, color.Bold).SprintFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprint
|
||||||
|
}()
|
||||||
|
colorYellowBold = func() func(format string, a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
return color.New(color.FgYellow, color.Bold).SprintfFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf
|
||||||
|
}()
|
||||||
|
colorBgYellow = func() func(format string, a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
return color.New(color.BgYellow).SprintfFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf
|
||||||
|
}()
|
||||||
|
colorBlack = func() func(format string, a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
return color.New(color.FgBlack).SprintfFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf
|
||||||
|
}()
|
||||||
|
colorGreenBold = func() func(format string, a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
return color.New(color.FgGreen, color.Bold).SprintfFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf
|
||||||
|
}()
|
||||||
|
colorRedBold = func() func(format string, a ...interface{}) string {
|
||||||
|
if isTerminal() {
|
||||||
|
return color.New(color.FgRed, color.Bold).SprintfFunc()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf
|
||||||
|
}()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns minio global information, as a key value map.
|
// Returns minio global information, as a key value map.
|
||||||
|
|||||||
+54
-25
@@ -17,8 +17,10 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -26,6 +28,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
"github.com/minio/minio/pkg/handlers"
|
||||||
httptracer "github.com/minio/minio/pkg/handlers"
|
httptracer "github.com/minio/minio/pkg/handlers"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -113,40 +116,54 @@ var userMetadataKeyPrefixes = []string{
|
|||||||
"X-Minio-Meta-",
|
"X-Minio-Meta-",
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractMetadataFromHeader extracts metadata from HTTP header.
|
// extractMetadata extracts metadata from HTTP header and HTTP queryString.
|
||||||
func extractMetadataFromHeader(ctx context.Context, header http.Header) (map[string]string, error) {
|
func extractMetadata(ctx context.Context, r *http.Request) (metadata map[string]string, err error) {
|
||||||
if header == nil {
|
query := r.URL.Query()
|
||||||
logger.LogIf(ctx, errInvalidArgument)
|
header := r.Header
|
||||||
return nil, errInvalidArgument
|
metadata = make(map[string]string)
|
||||||
|
// Extract all query values.
|
||||||
|
err = extractMetadataFromMap(ctx, query, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
metadata := make(map[string]string)
|
|
||||||
|
|
||||||
|
// Extract all header values.
|
||||||
|
err = extractMetadataFromMap(ctx, header, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success.
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractMetadata extracts metadata from map values.
|
||||||
|
func extractMetadataFromMap(ctx context.Context, v map[string][]string, m map[string]string) error {
|
||||||
|
if v == nil {
|
||||||
|
logger.LogIf(ctx, errInvalidArgument)
|
||||||
|
return errInvalidArgument
|
||||||
|
}
|
||||||
// Save all supported headers.
|
// Save all supported headers.
|
||||||
for _, supportedHeader := range supportedHeaders {
|
for _, supportedHeader := range supportedHeaders {
|
||||||
canonicalHeader := http.CanonicalHeaderKey(supportedHeader)
|
if value, ok := v[http.CanonicalHeaderKey(supportedHeader)]; ok {
|
||||||
// HTTP headers are case insensitive, look for both canonical
|
m[supportedHeader] = value[0]
|
||||||
// and non canonical entries.
|
} else if value, ok := v[supportedHeader]; ok {
|
||||||
if _, ok := header[canonicalHeader]; ok {
|
m[supportedHeader] = value[0]
|
||||||
metadata[supportedHeader] = header.Get(canonicalHeader)
|
|
||||||
} else if _, ok := header[supportedHeader]; ok {
|
|
||||||
metadata[supportedHeader] = header.Get(supportedHeader)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for key := range v {
|
||||||
// Go through all other headers for any additional headers that needs to be saved.
|
|
||||||
for key := range header {
|
|
||||||
if key != http.CanonicalHeaderKey(key) {
|
|
||||||
logger.LogIf(ctx, errInvalidArgument)
|
|
||||||
return nil, errInvalidArgument
|
|
||||||
}
|
|
||||||
for _, prefix := range userMetadataKeyPrefixes {
|
for _, prefix := range userMetadataKeyPrefixes {
|
||||||
if strings.HasPrefix(key, prefix) {
|
if !strings.HasPrefix(strings.ToLower(key), strings.ToLower(prefix)) {
|
||||||
metadata[key] = header.Get(key)
|
continue
|
||||||
|
}
|
||||||
|
value, ok := v[key]
|
||||||
|
if ok {
|
||||||
|
m[key] = strings.Join(value, ",")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return metadata, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Query string for the redirect URL the client is
|
// The Query string for the redirect URL the client is
|
||||||
@@ -167,7 +184,7 @@ func extractReqParams(r *http.Request) map[string]string {
|
|||||||
|
|
||||||
// Success.
|
// Success.
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"sourceIPAddress": r.RemoteAddr,
|
"sourceIPAddress": handlers.GetSourceIP(r),
|
||||||
// Add more fields here.
|
// Add more fields here.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,6 +237,19 @@ func extractPostPolicyFormValues(ctx context.Context, form *multipart.Form) (fil
|
|||||||
return nil, "", 0, nil, err
|
return nil, "", 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this means that filename="" was not specified for file key and Go has
|
||||||
|
// an ugly way of handling this situation. Refer here
|
||||||
|
// https://golang.org/src/mime/multipart/formdata.go#L61
|
||||||
|
if len(form.File) == 0 {
|
||||||
|
var b = &bytes.Buffer{}
|
||||||
|
for _, v := range formValues["File"] {
|
||||||
|
b.WriteString(v)
|
||||||
|
}
|
||||||
|
fileSize = int64(b.Len())
|
||||||
|
filePart = ioutil.NopCloser(b)
|
||||||
|
return filePart, fileName, fileSize, formValues, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Iterator until we find a valid File field and break
|
// Iterator until we find a valid File field and break
|
||||||
for k, v := range form.File {
|
for k, v := range form.File {
|
||||||
canonicalFormName := http.CanonicalHeaderKey(k)
|
canonicalFormName := http.CanonicalHeaderKey(k)
|
||||||
@@ -254,7 +284,6 @@ func extractPostPolicyFormValues(ctx context.Context, form *multipart.Form) (fil
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filePart, fileName, fileSize, formValues, nil
|
return filePart, fileName, fileSize, formValues, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user