mirror of
https://github.com/pgsty/minio.git
synced 2026-07-31 07:45:19 +03:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| e5e522fc61 | |||
| de251483d1 | |||
| 805186ab97 | |||
| ea76e72054 | |||
| 8bd7a19d50 | |||
| 25de775560 | |||
| abf209b1dd | |||
| d9d13c898c | |||
| ad79c626c6 | |||
| cd152f404a | |||
| 5fbdd70de9 | |||
| 0bbdd02a57 | |||
| 78abe5234e | |||
| f46ee54194 | |||
| 6005dbf01f | |||
| eb0e56ccf6 | |||
| c91abe6c4b | |||
| 670b538dde | |||
| 186000328e | |||
| 2575f4198a | |||
| 05a64dee95 | |||
| 577d10674d | |||
| 81ee79b042 | |||
| d94500ae26 | |||
| 001d9a4ae7 | |||
| c3a5146422 | |||
| 36c39d04da | |||
| 28d526bc68 | |||
| 05f96f3956 | |||
| cb9ee1584a | |||
| 9f4c120731 | |||
| 12a916091e | |||
| 842092f8de | |||
| 371349787f | |||
| 3dc13323e5 | |||
| 6ce7265c8c | |||
| f30c95a301 | |||
| 481390d51a | |||
| 6df1e4a529 | |||
| 853ea371ce | |||
| 7872c192ec | |||
| 39919708d6 | |||
| 7e12c3e8b9 |
+14
-18
@@ -8,27 +8,23 @@ dist: trusty
|
||||
|
||||
language: go
|
||||
|
||||
os:
|
||||
- linux
|
||||
|
||||
env:
|
||||
- ARCH=x86_64
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
env:
|
||||
- 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:
|
||||
- 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:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
go:
|
||||
- '1.10.1'
|
||||
|
||||
@@ -50,9 +50,11 @@ deadcode:
|
||||
@${GOPATH}/bin/deadcode -test $(shell go list ./...) || true
|
||||
|
||||
spelling:
|
||||
@${GOPATH}/bin/misspell -error `find cmd/`
|
||||
@${GOPATH}/bin/misspell -error `find pkg/`
|
||||
@${GOPATH}/bin/misspell -error `find docs/`
|
||||
@${GOPATH}/bin/misspell -locale US -error `find cmd/`
|
||||
@${GOPATH}/bin/misspell -locale US -error `find pkg/`
|
||||
@${GOPATH}/bin/misspell -locale US -error `find docs/`
|
||||
@${GOPATH}/bin/misspell -locale US -error `find buildscripts/`
|
||||
@${GOPATH}/bin/misspell -locale US -error `find dockerscripts/`
|
||||
|
||||
# Builds minio, runs the verifiers then runs the tests.
|
||||
check: test
|
||||
@@ -89,7 +91,7 @@ pkg-list:
|
||||
# Builds minio and installs it to $GOPATH/bin.
|
||||
install: build
|
||||
@echo "Installing minio binary to '$(GOPATH)/bin/minio'"
|
||||
@cp $(PWD)/minio $(GOPATH)/bin/minio
|
||||
@mkdir -p $(GOPATH)/bin && cp $(PWD)/minio $(GOPATH)/bin/minio
|
||||
@echo "Installation successful. To learn more, try \"minio --help\"."
|
||||
|
||||
clean:
|
||||
|
||||
@@ -25,14 +25,35 @@ import web from "../web"
|
||||
import { Redirect } from "react-router-dom"
|
||||
|
||||
export class Login extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
accessKey: "",
|
||||
secretKey: ""
|
||||
}
|
||||
}
|
||||
|
||||
// Handle field changes
|
||||
accessKeyChange(e) {
|
||||
this.setState({
|
||||
accessKey: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
secretKeyChange(e) {
|
||||
this.setState({
|
||||
secretKey: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault()
|
||||
const { showAlert, history } = this.props
|
||||
let message = ""
|
||||
if (!document.getElementById("accessKey").value) {
|
||||
if (this.state.accessKey === "") {
|
||||
message = "Access Key cannot be empty"
|
||||
}
|
||||
if (!document.getElementById("secretKey").value) {
|
||||
if (this.state.secretKey === "") {
|
||||
message = "Secret Key cannot be empty"
|
||||
}
|
||||
if (message) {
|
||||
@@ -41,8 +62,8 @@ export class Login extends React.Component {
|
||||
}
|
||||
web
|
||||
.Login({
|
||||
username: document.getElementById("accessKey").value,
|
||||
password: document.getElementById("secretKey").value
|
||||
username: this.state.accessKey,
|
||||
password: this.state.secretKey
|
||||
})
|
||||
.then(res => {
|
||||
history.push("/")
|
||||
@@ -77,6 +98,8 @@ export class Login extends React.Component {
|
||||
<div className="l-wrap">
|
||||
<form onSubmit={this.handleSubmit.bind(this)}>
|
||||
<InputGroup
|
||||
value={this.state.accessKey}
|
||||
onChange={this.accessKeyChange.bind(this)}
|
||||
className="ig-dark"
|
||||
label="Access Key"
|
||||
id="accessKey"
|
||||
@@ -87,6 +110,8 @@ export class Login extends React.Component {
|
||||
autoComplete="username"
|
||||
/>
|
||||
<InputGroup
|
||||
value={this.state.secretKey}
|
||||
onChange={this.secretKeyChange.bind(this)}
|
||||
className="ig-dark"
|
||||
label="Secret Key"
|
||||
id="secretKey"
|
||||
|
||||
@@ -60,20 +60,25 @@ describe("Login", () => {
|
||||
alert={{ show: false, type: "danger"}}
|
||||
showAlert={showAlertMock}
|
||||
clearAlert={clearAlertMock}
|
||||
/>,
|
||||
{ attachTo: document.body }
|
||||
/>
|
||||
)
|
||||
// case where both keys are empty - displays the second warning
|
||||
wrapper.find("form").simulate("submit")
|
||||
expect(showAlertMock).toHaveBeenCalledWith("danger", "Secret Key cannot be empty")
|
||||
|
||||
// case where access key is empty
|
||||
document.getElementById("secretKey").value = "secretKey"
|
||||
wrapper.setState({
|
||||
accessKey: "",
|
||||
secretKey: "secretKey"
|
||||
})
|
||||
wrapper.find("form").simulate("submit")
|
||||
expect(showAlertMock).toHaveBeenCalledWith("danger", "Access Key cannot be empty")
|
||||
|
||||
// case where secret key is empty
|
||||
document.getElementById("accessKey").value = "accessKey"
|
||||
wrapper.setState({
|
||||
accessKey: "accessKey",
|
||||
secretKey: ""
|
||||
})
|
||||
wrapper.find("form").simulate("submit")
|
||||
expect(showAlertMock).toHaveBeenCalledWith("danger", "Secret Key cannot be empty")
|
||||
})
|
||||
@@ -85,11 +90,12 @@ describe("Login", () => {
|
||||
alert={{ show: false, type: "danger"}}
|
||||
showAlert={showAlertMock}
|
||||
clearAlert={clearAlertMock}
|
||||
/>,
|
||||
{ attachTo: document.body }
|
||||
/>
|
||||
)
|
||||
document.getElementById("accessKey").value = "accessKey"
|
||||
document.getElementById("secretKey").value = "secretKey"
|
||||
wrapper.setState({
|
||||
accessKey: "accessKey",
|
||||
secretKey: "secretKey"
|
||||
})
|
||||
wrapper.find("form").simulate("submit")
|
||||
expect(web.Login).toHaveBeenCalledWith({
|
||||
"username": "accessKey",
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import React from "react"
|
||||
import classNames from "classnames"
|
||||
import { connect } from "react-redux"
|
||||
import humanize from "humanize"
|
||||
import Moment from "moment"
|
||||
@@ -35,22 +34,19 @@ export const ObjectItem = ({
|
||||
onClick
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"fesl-row " +
|
||||
classNames({
|
||||
"fesl-row-selected": checked
|
||||
})
|
||||
}
|
||||
data-type={getDataType(name, contentType)}
|
||||
>
|
||||
<div className={"fesl-row"} data-type={getDataType(name, contentType)}>
|
||||
<div className="fesl-item fesl-item-icon">
|
||||
<div className="fi-select">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
checked={checked}
|
||||
onChange={() => {
|
||||
checked ? uncheckObject(name) : checkObject(name)
|
||||
}}
|
||||
/>
|
||||
<i className="fis-icon" />
|
||||
<i className="fis-helper" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-name">
|
||||
@@ -58,7 +54,9 @@ export const ObjectItem = ({
|
||||
href="#"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
checked ? uncheckObject(name) : checkObject(name)
|
||||
if (onClick) {
|
||||
onClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
|
||||
@@ -29,10 +29,10 @@ export class ObjectsBulkActions extends React.Component {
|
||||
}
|
||||
}
|
||||
handleDownload() {
|
||||
const { checkedObjects, resetCheckedList, downloadChecked, downloadObject } = this.props
|
||||
if (checkedObjects.length === 1) {
|
||||
const { checkedObjects, clearChecked, downloadChecked, downloadObject } = this.props
|
||||
if (checkedObjects.length === 1 && !checkedObjects[0].endsWith('/')) {
|
||||
downloadObject(checkedObjects[0])
|
||||
resetCheckedList()
|
||||
clearChecked()
|
||||
} else {
|
||||
downloadChecked()
|
||||
}
|
||||
@@ -59,17 +59,16 @@ export class ObjectsBulkActions extends React.Component {
|
||||
}
|
||||
>
|
||||
<span className="la-label">
|
||||
<i className="fa fa-check-circle" /> {checkedObjects.length}
|
||||
<i className="fa fa-check-circle" /> {checkedObjects.length}
|
||||
{checkedObjects.length === 1 ? " Object " : " Objects "}
|
||||
selected
|
||||
</span>
|
||||
<span className="la-actions pull-right">
|
||||
<button
|
||||
id="download-checked"
|
||||
onClick={this.handleDownload.bind(this)}
|
||||
>
|
||||
<button id="download-checked" onClick={this.handleDownload.bind(this)}>
|
||||
{" "}
|
||||
Download {checkedObjects.length === 1 ? "object" : "all as zip"}{" "}
|
||||
Download
|
||||
{(checkedObjects.length === 1 && !checkedObjects[0].endsWith('/')) ?
|
||||
" object" : " all as zip" }{" "}
|
||||
</button>
|
||||
</span>
|
||||
<span className="la-actions pull-right">
|
||||
@@ -105,6 +104,7 @@ const mapStateToProps = state => {
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
downloadObject: object => dispatch(actions.downloadObject(object)),
|
||||
downloadChecked: () => dispatch(actions.downloadCheckedObjects()),
|
||||
downloadObject: object => dispatch(actions.downloadObject(object)),
|
||||
resetCheckedList: () => dispatch(actions.resetCheckedList()),
|
||||
|
||||
@@ -28,22 +28,40 @@ describe("ObjectItem", () => {
|
||||
expect(wrapper.prop("data-type")).toBe("image")
|
||||
})
|
||||
|
||||
it("should call checkObject when the object is selected", () => {
|
||||
const checkObject = jest.fn()
|
||||
const wrapper = shallow(<ObjectItem name={"test"} checked={false} checkObject={checkObject} />)
|
||||
it("shouldn't call onClick when the object isclicked", () => {
|
||||
const onClick = jest.fn()
|
||||
const wrapper = shallow(<ObjectItem name={"test"} />)
|
||||
wrapper.find("a").simulate("click", { preventDefault: jest.fn() })
|
||||
expect(onClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call onClick when the folder isclicked", () => {
|
||||
const onClick = jest.fn()
|
||||
const wrapper = shallow(<ObjectItem name={"test/"} onClick={onClick} />)
|
||||
wrapper.find("a").simulate("click", { preventDefault: jest.fn() })
|
||||
expect(onClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call checkObject when the object/prefix is checked", () => {
|
||||
const checkObject = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectItem name={"test"} checked={false} checkObject={checkObject} />
|
||||
)
|
||||
wrapper.find("input[type='checkbox']").simulate("change")
|
||||
expect(checkObject).toHaveBeenCalledWith("test")
|
||||
})
|
||||
|
||||
it("should render highlighted row when object is selected", () => {
|
||||
it("should render checked checkbox", () => {
|
||||
const wrapper = shallow(<ObjectItem name={"test"} checked={true} />)
|
||||
expect(wrapper.find(".fesl-row").hasClass("fesl-row-selected")).toBeTruthy()
|
||||
expect(wrapper.find("input[type='checkbox']").prop("checked")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should call uncheckObject when the object is deselected", () => {
|
||||
it("should call uncheckObject when the object/prefix is unchecked", () => {
|
||||
const uncheckObject = jest.fn()
|
||||
const wrapper = shallow(<ObjectItem name={"test"} checked={true} uncheckObject={uncheckObject} />)
|
||||
wrapper.find("a").simulate("click", { preventDefault: jest.fn() })
|
||||
const wrapper = shallow(
|
||||
<ObjectItem name={"test"} checked={true} uncheckObject={uncheckObject} />
|
||||
)
|
||||
wrapper.find("input[type='checkbox']").simulate("change")
|
||||
expect(uncheckObject).toHaveBeenCalledWith("test")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ObjectsBulkActions } from "../ObjectsBulkActions"
|
||||
|
||||
describe("ObjectsBulkActions", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<ObjectsBulkActions checkedObjects={0} />)
|
||||
shallow(<ObjectsBulkActions checkedObjects={[]} />)
|
||||
})
|
||||
|
||||
it("should show actions when checkObjectsCount is more than 0", () => {
|
||||
@@ -28,21 +28,33 @@ describe("ObjectsBulkActions", () => {
|
||||
expect(wrapper.hasClass("list-actions-toggled")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should call downloadObject for single object when download button is clicked", () => {
|
||||
it("should call downloadObject when single object is selected and download button is clicked", () => {
|
||||
const downloadObject = jest.fn()
|
||||
const resetCheckedList = jest.fn()
|
||||
const clearChecked = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsBulkActions
|
||||
checkedObjects={["test1"]}
|
||||
checkedObjects={["test"]}
|
||||
downloadObject={downloadObject}
|
||||
resetCheckedList={resetCheckedList}
|
||||
clearChecked={clearChecked}
|
||||
/>
|
||||
)
|
||||
wrapper.find("#download-checked").simulate("click")
|
||||
expect(downloadObject).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call downloadChecked for multiple objects when download button is clicked", () => {
|
||||
it("should call downloadChecked when a folder is selected and download button is clicked", () => {
|
||||
const downloadChecked = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsBulkActions
|
||||
checkedObjects={["test/"]}
|
||||
downloadChecked={downloadChecked}
|
||||
/>
|
||||
)
|
||||
wrapper.find("#download-checked").simulate("click")
|
||||
expect(downloadChecked).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call downloadChecked when multiple objects are selected and download button is clicked", () => {
|
||||
const downloadChecked = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsBulkActions
|
||||
|
||||
@@ -74,7 +74,7 @@ div.fesl-row {
|
||||
border-bottom: 1px solid transparent;
|
||||
cursor: default;
|
||||
.transition(background-color);
|
||||
.transition-duration(300ms);
|
||||
.transition-duration(500ms);
|
||||
|
||||
@media (max-width: (@screen-xs-max - 100px)) {
|
||||
padding: 5px 20px;
|
||||
@@ -87,8 +87,22 @@ div.fesl-row {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(.fesl-row-selected) {
|
||||
background: lighten(@text-muted-color, 22%);
|
||||
.fis-icon {
|
||||
&:before {
|
||||
.opacity(0)
|
||||
}
|
||||
}
|
||||
|
||||
.fis-helper {
|
||||
&:before {
|
||||
.opacity(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fesl-item-name {
|
||||
a {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,14 +143,6 @@ div.fesl-row {
|
||||
.fesl-row-selected {
|
||||
background-color: @list-row-selected-bg;
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: @list-row-selected-even-bg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: @list-row-selected-hover;
|
||||
}
|
||||
|
||||
&, .fesl-item a {
|
||||
color: darken(@text-color, 10%);
|
||||
}
|
||||
@@ -161,6 +167,27 @@ div.fesl-row {
|
||||
height: 35px;
|
||||
z-index: 8;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:checked {
|
||||
& ~ .fis-icon {
|
||||
background-color: #32393F;
|
||||
|
||||
&:before {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& ~ .fis-helper {
|
||||
&:before {
|
||||
.scale(0);
|
||||
}
|
||||
|
||||
&:after {
|
||||
.scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +216,38 @@ div.fesl-row {
|
||||
}
|
||||
}
|
||||
|
||||
.fis-helper {
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
.transition(all);
|
||||
.transition-duration(250ms);
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 2px solid @white;
|
||||
z-index: 7;
|
||||
border-radius: 2px;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
font-family: @font-family-icon;
|
||||
content: '\f00c';
|
||||
top: 8px;
|
||||
left: 9px;
|
||||
color: @white;
|
||||
font-size: 14px;
|
||||
.scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*--------------------------
|
||||
Files and Folders
|
||||
----------------------------*/
|
||||
|
||||
@@ -97,7 +97,5 @@
|
||||
/*-------------------------
|
||||
List
|
||||
--------------------------*/
|
||||
@list-row-selected-bg: #fffad6;
|
||||
@list-row-selected-even-bg: #faf5d1;
|
||||
@list-row-selected-hover: #f5f0cc;
|
||||
@list-row-selected-bg: #fbf2bf;
|
||||
@list-row-even-bg: #fafafa;
|
||||
+24
-24
File diff suppressed because one or more lines are too long
@@ -34,7 +34,7 @@ _init() {
|
||||
|
||||
## FIXME:
|
||||
## In OSX, 'readlink -f' option does not exist, hence
|
||||
## we have our own readlink -f behaviour here.
|
||||
## we have our own readlink -f behavior here.
|
||||
## Once OSX has the option, below function is good enough.
|
||||
##
|
||||
## readlink() {
|
||||
@@ -89,11 +89,11 @@ check_minimum_version() {
|
||||
|
||||
assert_is_supported_arch() {
|
||||
case "${ARCH}" in
|
||||
x86_64 | amd64 | aarch64 | arm* )
|
||||
x86_64 | amd64 | aarch64 | ppc64le | arm* )
|
||||
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*]"
|
||||
exit 1
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func genLDFlags(version string) string {
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.CommitID=" + commitID()
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.ShortCommitID=" + commitID()[:12]
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.GOPATH=" + os.Getenv("GOPATH")
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.GOROOT=" + os.Getenv("GOROOT")
|
||||
return ldflagsStr
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ type accessControlPolicy struct {
|
||||
// This operation uses the ACL
|
||||
// subresource to return the ACL of a specified bucket.
|
||||
func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "GetBucketACL")
|
||||
ctx := newContext(r, w, "GetBucketACL")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -101,7 +101,7 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
|
||||
// This operation uses the ACL
|
||||
// subresource to return the ACL of a specified object.
|
||||
func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "GetObjectACL")
|
||||
ctx := newContext(r, w, "GetObjectACL")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
+10
-5
@@ -356,7 +356,7 @@ func (a adminAPIHandlers) ListLocksHandler(w http.ResponseWriter, r *http.Reques
|
||||
// ---------
|
||||
// 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")
|
||||
ctx := newContext(r, w, "ClearLocks")
|
||||
|
||||
// Get object layer instance.
|
||||
objLayer := newObjectLayerFn()
|
||||
@@ -464,7 +464,7 @@ func extractHealInitParams(r *http.Request) (bucket, objPrefix string,
|
||||
// sequence. However, if the force-start flag is provided, the server
|
||||
// aborts the running heal sequence and starts a new one.
|
||||
func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "Heal")
|
||||
ctx := newContext(r, w, "Heal")
|
||||
|
||||
// Get object layer instance.
|
||||
objLayer := newObjectLayerFn()
|
||||
@@ -474,7 +474,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
adminAPIErr := checkAdminRequestAuthType(r, globalServerConfig.GetRegion())
|
||||
adminAPIErr := checkAdminRequestAuthType(r, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(w, adminAPIErr, r.URL)
|
||||
return
|
||||
@@ -712,7 +712,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
err = json.Unmarshal(configBytes, &config)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(w, toAPIErrorCode(err), r.URL)
|
||||
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -727,6 +727,11 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
writeCustomErrorResponseJSON(w, ErrAdminConfigBadJSON, err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write config received from request onto a temporary file on
|
||||
// all nodes.
|
||||
tmpFileName := fmt.Sprintf(minioConfigTmpFormat, mustGetUUID())
|
||||
@@ -818,7 +823,7 @@ func (a adminAPIHandlers) UpdateCredentialsHandler(w http.ResponseWriter,
|
||||
|
||||
// Update local credentials in memory.
|
||||
globalServerConfig.SetCredential(creds)
|
||||
if err = globalServerConfig.Save(); err != nil {
|
||||
if err = globalServerConfig.Save(getConfigFile()); err != nil {
|
||||
writeErrorResponseJSON(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
+77
-36
@@ -38,22 +38,24 @@ import (
|
||||
|
||||
var (
|
||||
configJSON = []byte(`{
|
||||
"version": "13",
|
||||
"version": "27",
|
||||
"credential": {
|
||||
"accessKey": "minio",
|
||||
"secretKey": "minio123"
|
||||
},
|
||||
"region": "us-west-1",
|
||||
"logger": {
|
||||
"console": {
|
||||
"enable": true,
|
||||
"level": "fatal"
|
||||
},
|
||||
"file": {
|
||||
"enable": false,
|
||||
"fileName": "",
|
||||
"level": ""
|
||||
}
|
||||
"region": "",
|
||||
"browser": "on",
|
||||
"worm": "off",
|
||||
"domain": "",
|
||||
"storageclass": {
|
||||
"standard": "",
|
||||
"rrs": ""
|
||||
},
|
||||
"cache": {
|
||||
"drives": [],
|
||||
"expiry": 90,
|
||||
"maxuse": 80,
|
||||
"exclude": []
|
||||
},
|
||||
"notify": {
|
||||
"amqp": {
|
||||
@@ -63,6 +65,7 @@ var (
|
||||
"exchange": "",
|
||||
"routingKey": "",
|
||||
"exchangeType": "",
|
||||
"deliveryMode": 0,
|
||||
"mandatory": false,
|
||||
"immediate": false,
|
||||
"durable": false,
|
||||
@@ -71,6 +74,47 @@ var (
|
||||
"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": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
@@ -90,24 +134,10 @@ var (
|
||||
}
|
||||
}
|
||||
},
|
||||
"elasticsearch": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"url": "",
|
||||
"index": ""
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"postgresql": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"format": "",
|
||||
"connectionString": "",
|
||||
"table": "",
|
||||
"host": "",
|
||||
@@ -117,11 +147,13 @@ var (
|
||||
"database": ""
|
||||
}
|
||||
},
|
||||
"kafka": {
|
||||
"redis": {
|
||||
"1": {
|
||||
"enable": false,
|
||||
"brokers": null,
|
||||
"topic": ""
|
||||
"format": "",
|
||||
"address": "",
|
||||
"password": "",
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
@@ -130,8 +162,20 @@ var (
|
||||
"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
|
||||
@@ -176,10 +220,7 @@ func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
||||
// Init global heal state
|
||||
initAllHealState(globalIsXL)
|
||||
|
||||
globalNotificationSys, err = NewNotificationSys(globalServerConfig, globalEndpoints)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
|
||||
|
||||
// Create new policy system.
|
||||
globalPolicySys = NewPolicySys()
|
||||
|
||||
@@ -57,7 +57,7 @@ const (
|
||||
var (
|
||||
errHealIdleTimeout = fmt.Errorf("healing results were not consumed for too long")
|
||||
errHealPushStopNDiscard = fmt.Errorf("heal push stopped due to heal stop signal")
|
||||
errHealStopSignalled = fmt.Errorf("heal stop signalled")
|
||||
errHealStopSignalled = fmt.Errorf("heal stop signaled")
|
||||
|
||||
errFnHealFromAPIErr = func(err error) error {
|
||||
errCode := toAPIErrorCode(err)
|
||||
@@ -301,7 +301,7 @@ type healSequence struct {
|
||||
// current accumulated status of the heal sequence
|
||||
currentStatus healSequenceStatus
|
||||
|
||||
// channel signalled by background routine when traversal has
|
||||
// channel signaled by background routine when traversal has
|
||||
// completed
|
||||
traverseAndHealDoneCh chan error
|
||||
|
||||
@@ -441,7 +441,7 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
||||
h.currentStatus.updateLock.Unlock()
|
||||
|
||||
// This is a "safe" point for the heal sequence to quit if
|
||||
// signalled externally.
|
||||
// signaled externally.
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
@@ -179,9 +179,9 @@ func makeAdminPeers(endpoints EndpointList) (adminPeerList adminPeers) {
|
||||
|
||||
for _, hostStr := range GetRemotePeers(endpoints) {
|
||||
host, err := xnet.ParseHost(hostStr)
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to parse Admin RPC Host", context.Background())
|
||||
rpcClient, err := NewAdminRPCClient(host)
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to initialize Admin RPC Client", context.Background())
|
||||
adminPeerList = append(adminPeerList, adminPeer{
|
||||
addr: hostStr,
|
||||
cmdRunner: rpcClient,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func NewAdminRPCServer() (*xrpc.Server, error) {
|
||||
// registerAdminRPCRouter - creates and registers Admin RPC server and its router.
|
||||
func registerAdminRPCRouter(router *mux.Router) {
|
||||
rpcServer, err := NewAdminRPCServer()
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server", context.Background())
|
||||
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
subrouter.Path(adminServiceSubPath).Handler(rpcServer)
|
||||
}
|
||||
|
||||
@@ -139,10 +139,8 @@ func testAdminCmdRunnerServerInfo(t *testing.T, client adminCmdRunner) {
|
||||
}()
|
||||
|
||||
endpoints := new(EndpointList)
|
||||
notificationSys, err := NewNotificationSys(globalServerConfig, *endpoints)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
notificationSys := NewNotificationSys(globalServerConfig, *endpoints)
|
||||
|
||||
testCases := []struct {
|
||||
bootTime time.Time
|
||||
|
||||
+40
-35
@@ -17,11 +17,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
@@ -38,8 +40,8 @@ type APIErrorResponse struct {
|
||||
XMLName xml.Name `xml:"Error" json:"-"`
|
||||
Code string
|
||||
Message string
|
||||
Key string
|
||||
BucketName string
|
||||
Key string `xml:"Key,omitempty" json:"Key,omitempty"`
|
||||
BucketName string `xml:"BucketName,omitempty" json:"BucketName,omitempty"`
|
||||
Resource string
|
||||
RequestID string `xml:"RequestId" json:"RequestId"`
|
||||
HostID string `xml:"HostId" json:"HostId"`
|
||||
@@ -167,7 +169,6 @@ const (
|
||||
ErrInvalidResourceName
|
||||
ErrServerNotInitialized
|
||||
ErrOperationTimedOut
|
||||
ErrPartsSizeUnequal
|
||||
ErrInvalidRequest
|
||||
// Minio storage class error codes
|
||||
ErrInvalidStorageClass
|
||||
@@ -191,6 +192,7 @@ const (
|
||||
ErrHealMissingBucket
|
||||
ErrHealAlreadyRunning
|
||||
ErrHealOverlappingPaths
|
||||
ErrIncorrectContinuationToken
|
||||
)
|
||||
|
||||
// error code to APIError structure, these fields carry respective
|
||||
@@ -689,7 +691,7 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
ErrStorageFull: {
|
||||
Code: "XMinioStorageFull",
|
||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
HTTPStatusCode: http.StatusInsufficientStorage,
|
||||
},
|
||||
ErrRequestBodyParse: {
|
||||
Code: "XMinioRequestBodyParse",
|
||||
@@ -782,11 +784,6 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Your metadata headers are not supported.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrPartsSizeUnequal: {
|
||||
Code: "XMinioPartsSizeUnequal",
|
||||
Description: "All parts except the last part should be of the same size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrObjectTampered: {
|
||||
Code: "XMinioObjectTampered",
|
||||
Description: errObjectTampered.Error(),
|
||||
@@ -840,7 +837,11 @@ var errorCodeResponse = map[APIErrorCode]APIError{
|
||||
Description: "Object storage backend is unreachable",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
|
||||
ErrIncorrectContinuationToken: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "The continuation token provided is incorrect",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// Add your error structure here.
|
||||
}
|
||||
|
||||
@@ -864,6 +865,29 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrAdminInvalidAccessKey
|
||||
case auth.ErrInvalidSecretKeyLength:
|
||||
apiErr = ErrAdminInvalidSecretKey
|
||||
// SSE errors
|
||||
case errInsecureSSERequest:
|
||||
apiErr = ErrInsecureSSECustomerRequest
|
||||
case errInvalidSSEAlgorithm:
|
||||
apiErr = ErrInvalidSSECustomerAlgorithm
|
||||
case errInvalidSSEKey:
|
||||
apiErr = ErrInvalidSSECustomerKey
|
||||
case errMissingSSEKey:
|
||||
apiErr = ErrMissingSSECustomerKey
|
||||
case errMissingSSEKeyMD5:
|
||||
apiErr = ErrMissingSSECustomerKeyMD5
|
||||
case errSSEKeyMD5Mismatch:
|
||||
apiErr = ErrSSECustomerKeyMD5Mismatch
|
||||
case errObjectTampered:
|
||||
apiErr = ErrObjectTampered
|
||||
case errEncryptedObject:
|
||||
apiErr = ErrSSEEncryptedObject
|
||||
case errInvalidSSEParameters:
|
||||
apiErr = ErrInvalidSSECustomerParameters
|
||||
case errSSEKeyMismatch:
|
||||
apiErr = ErrAccessDenied // no access without correct key
|
||||
case context.Canceled, context.DeadlineExceeded:
|
||||
apiErr = ErrOperationTimedOut
|
||||
}
|
||||
|
||||
if apiErr != ErrNone {
|
||||
@@ -871,27 +895,10 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
switch err { // SSE errors
|
||||
case errInsecureSSERequest:
|
||||
return ErrInsecureSSECustomerRequest
|
||||
case errInvalidSSEAlgorithm:
|
||||
return ErrInvalidSSECustomerAlgorithm
|
||||
case errInvalidSSEKey:
|
||||
return ErrInvalidSSECustomerKey
|
||||
case errMissingSSEKey:
|
||||
return ErrMissingSSECustomerKey
|
||||
case errMissingSSEKeyMD5:
|
||||
return ErrMissingSSECustomerKeyMD5
|
||||
case errSSEKeyMD5Mismatch:
|
||||
return ErrSSECustomerKeyMD5Mismatch
|
||||
case errObjectTampered:
|
||||
return ErrObjectTampered
|
||||
case errEncryptedObject:
|
||||
return ErrSSEEncryptedObject
|
||||
case errInvalidSSEParameters:
|
||||
return ErrInvalidSSECustomerParameters
|
||||
case errSSEKeyMismatch:
|
||||
return ErrAccessDenied // no access without correct key
|
||||
// etcd specific errors, a key is always a bucket for us return
|
||||
// ErrNoSuchBucket in such a case.
|
||||
if err == dns.ErrNoEntriesFound {
|
||||
return ErrNoSuchBucket
|
||||
}
|
||||
|
||||
switch err.(type) {
|
||||
@@ -957,8 +964,6 @@ func toAPIErrorCode(err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrEntityTooLarge
|
||||
case UnsupportedMetadata:
|
||||
apiErr = ErrUnsupportedMetadata
|
||||
case PartsSizeUnequal:
|
||||
apiErr = ErrPartsSizeUnequal
|
||||
case BucketPolicyNotFound:
|
||||
apiErr = ErrNoSuchBucketPolicy
|
||||
case *event.ErrInvalidEventName:
|
||||
@@ -999,12 +1004,12 @@ func getAPIError(code APIErrorCode) APIError {
|
||||
|
||||
// getErrorResponse gets in standard error and resource value and
|
||||
// provides a encodable populated response values
|
||||
func getAPIErrorResponse(err APIError, resource string) APIErrorResponse {
|
||||
func getAPIErrorResponse(err APIError, resource, requestid string) APIErrorResponse {
|
||||
return APIErrorResponse{
|
||||
Code: err.Code,
|
||||
Message: err.Description,
|
||||
Resource: resource,
|
||||
RequestID: "3L137",
|
||||
RequestID: requestid,
|
||||
HostID: "3L137",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ func mustGetRequestID(t time.Time) string {
|
||||
|
||||
// Write http common headers
|
||||
func setCommonHeaders(w http.ResponseWriter) {
|
||||
// Set unique request ID for each reply.
|
||||
w.Header().Set(responseRequestIDKey, mustGetRequestID(UTCNow()))
|
||||
w.Header().Set("Server", globalServerUserAgent)
|
||||
// Set `x-amz-bucket-region` only if region is set on the server
|
||||
// by default minio uses an empty region.
|
||||
|
||||
+11
-1
@@ -36,7 +36,17 @@ func getListObjectsV1Args(values url.Values) (prefix, marker, delimiter string,
|
||||
}
|
||||
|
||||
// Parse bucket url queries for ListObjects V2.
|
||||
func getListObjectsV2Args(values url.Values) (prefix, token, startAfter, delimiter string, fetchOwner bool, maxkeys int, encodingType string) {
|
||||
func getListObjectsV2Args(values url.Values) (prefix, token, startAfter, delimiter string, fetchOwner bool, maxkeys int, encodingType string, errCode APIErrorCode) {
|
||||
errCode = ErrNone
|
||||
|
||||
// The continuation-token cannot be empty.
|
||||
if val, ok := values["continuation-token"]; ok {
|
||||
if len(val[0]) == 0 {
|
||||
errCode = ErrIncorrectContinuationToken
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
prefix = values.Get("prefix")
|
||||
token = values.Get("continuation-token")
|
||||
startAfter = values.Get("start-after")
|
||||
|
||||
@@ -29,6 +29,7 @@ func TestListObjectsV2Resources(t *testing.T) {
|
||||
fetchOwner bool
|
||||
maxKeys int
|
||||
encodingType string
|
||||
errCode APIErrorCode
|
||||
}{
|
||||
{
|
||||
values: url.Values{
|
||||
@@ -47,6 +48,7 @@ func TestListObjectsV2Resources(t *testing.T) {
|
||||
fetchOwner: true,
|
||||
maxKeys: 100,
|
||||
encodingType: "gzip",
|
||||
errCode: ErrNone,
|
||||
},
|
||||
{
|
||||
values: url.Values{
|
||||
@@ -64,11 +66,34 @@ func TestListObjectsV2Resources(t *testing.T) {
|
||||
fetchOwner: true,
|
||||
maxKeys: 1000,
|
||||
encodingType: "gzip",
|
||||
errCode: ErrNone,
|
||||
},
|
||||
{
|
||||
values: url.Values{
|
||||
"prefix": []string{"photos/"},
|
||||
"continuation-token": []string{""},
|
||||
"start-after": []string{"start-after"},
|
||||
"delimiter": []string{"/"},
|
||||
"fetch-owner": []string{"true"},
|
||||
"encoding-type": []string{"gzip"},
|
||||
},
|
||||
prefix: "",
|
||||
token: "",
|
||||
startAfter: "",
|
||||
delimiter: "",
|
||||
fetchOwner: false,
|
||||
maxKeys: 0,
|
||||
encodingType: "",
|
||||
errCode: ErrIncorrectContinuationToken,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, encodingType := getListObjectsV2Args(testCase.values)
|
||||
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, encodingType, errCode := getListObjectsV2Args(testCase.values)
|
||||
|
||||
if errCode != testCase.errCode {
|
||||
t.Errorf("Test %d: Expected error code:%d, got %d", i+1, testCase.errCode, errCode)
|
||||
}
|
||||
if prefix != testCase.prefix {
|
||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.prefix, prefix)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func writePartSmallErrorResponse(w http.ResponseWriter, r *http.Request, err Par
|
||||
|
||||
apiError := getAPIError(toAPIErrorCode(err))
|
||||
// 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}
|
||||
encodedErrorResponse := encodeResponse(cmpErrResp)
|
||||
|
||||
|
||||
+7
-4
@@ -21,6 +21,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
@@ -293,8 +294,10 @@ func getObjectLocation(r *http.Request, domain, bucket, object string) string {
|
||||
}
|
||||
// If domain is set then we need to use bucket DNS style.
|
||||
if domain != "" {
|
||||
u.Host = bucket + "." + domain
|
||||
u.Path = path.Join(slashSeparator, object)
|
||||
if strings.Contains(r.Host, domain) {
|
||||
u.Host = bucket + "." + r.Host
|
||||
u.Path = path.Join(slashSeparator, object)
|
||||
}
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
@@ -574,7 +577,7 @@ func writeErrorResponse(w http.ResponseWriter, errorCode APIErrorCode, reqURL *u
|
||||
}
|
||||
apiError := getAPIError(errorCode)
|
||||
// Generate error response.
|
||||
errorResponse := getAPIErrorResponse(apiError, reqURL.Path)
|
||||
errorResponse := getAPIErrorResponse(apiError, reqURL.Path, w.Header().Get(responseRequestIDKey))
|
||||
encodedErrorResponse := encodeResponse(errorResponse)
|
||||
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeXML)
|
||||
}
|
||||
@@ -589,7 +592,7 @@ func writeErrorResponseHeadersOnly(w http.ResponseWriter, errorCode APIErrorCode
|
||||
func writeErrorResponseJSON(w http.ResponseWriter, errorCode APIErrorCode, reqURL *url.URL) {
|
||||
apiError := getAPIError(errorCode)
|
||||
// Generate error response.
|
||||
errorResponse := getAPIErrorResponse(apiError, reqURL.Path)
|
||||
errorResponse := getAPIErrorResponse(apiError, reqURL.Path, w.Header().Get(responseRequestIDKey))
|
||||
encodedErrorResponse := encodeResponseJSON(errorResponse)
|
||||
writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeJSON)
|
||||
}
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ func registerAPIRouter(router *mux.Router) {
|
||||
// HeadBucket
|
||||
bucket.Methods("HEAD").HandlerFunc(httpTraceAll(api.HeadBucketHandler))
|
||||
// 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
|
||||
bucket.Methods("POST").HandlerFunc(httpTraceAll(api.DeleteMultipleObjectsHandler)).Queries("delete", "")
|
||||
// DeleteBucketPolicy
|
||||
|
||||
@@ -54,7 +54,7 @@ func validateListObjectsArgs(prefix, marker, delimiter string, maxKeys int) APIE
|
||||
// NOTE: It is recommended that this API to be used for application development.
|
||||
// Minio continues to support ListObjectsV1 for supporting legacy tools.
|
||||
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "ListObjectsV2")
|
||||
ctx := newContext(r, w, "ListObjectsV2")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -70,20 +70,19 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
return
|
||||
}
|
||||
|
||||
// Extract all the listObjectsV2 query params to their native values.
|
||||
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, _ := getListObjectsV2Args(r.URL.Query())
|
||||
urlValues := r.URL.Query()
|
||||
|
||||
// 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
|
||||
// Extract all the listObjectsV2 query params to their native values.
|
||||
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, _, errCode := getListObjectsV2Args(urlValues)
|
||||
|
||||
if errCode != ErrNone {
|
||||
writeErrorResponse(w, errCode, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the query params before beginning to serve the request.
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
@@ -93,8 +92,8 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
}
|
||||
// Inititate a list objects operation based on the input params.
|
||||
// On success would return back ListObjectsInfo object to be
|
||||
// marshalled into S3 compatible XML header.
|
||||
listObjectsV2Info, err := listObjectsV2(ctx, bucket, prefix, marker, delimiter, maxKeys, fetchOwner, startAfter)
|
||||
// marshaled into S3 compatible XML header.
|
||||
listObjectsV2Info, err := listObjectsV2(ctx, bucket, prefix, token, delimiter, maxKeys, fetchOwner, startAfter)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -124,7 +123,7 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
||||
// criteria to return a subset of the objects in a bucket.
|
||||
//
|
||||
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "ListObjectsV1")
|
||||
ctx := newContext(r, w, "ListObjectsV1")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -159,7 +158,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||
}
|
||||
// Inititate a list objects operation based on the input params.
|
||||
// On success would return back ListObjectsInfo object to be
|
||||
// marshalled into S3 compatible XML header.
|
||||
// marshaled into S3 compatible XML header.
|
||||
listObjectsInfo, err := listObjects(ctx, bucket, prefix, marker, delimiter, maxKeys)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
|
||||
+146
-60
@@ -17,8 +17,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -26,20 +28,65 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
)
|
||||
|
||||
// Check if there are buckets on server without corresponding entry in etcd backend and
|
||||
// make entries. Here is the general flow
|
||||
// - Range over all the available buckets
|
||||
// - Check if a bucket has an entry in etcd backend
|
||||
// -- If no, make an entry
|
||||
// -- If yes, check if the IP of entry matches local IP. This means entry is for this instance.
|
||||
// -- If IP of the entry doesn't match, this means entry is for another instance. Log an error to console.
|
||||
func initFederatorBackend(objLayer ObjectLayer) {
|
||||
b, err := objLayer.ListBuckets(context.Background())
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return
|
||||
}
|
||||
|
||||
g := errgroup.WithNErrs(len(b))
|
||||
for index := range b {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
r, gerr := globalDNSConfig.Get(b[index].Name)
|
||||
if gerr != nil {
|
||||
if gerr == dns.ErrNoEntriesFound {
|
||||
return globalDNSConfig.Put(b[index].Name)
|
||||
}
|
||||
return gerr
|
||||
}
|
||||
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
|
||||
// There is already an entry for this bucket, with all IP addresses different. This indicates a bucket name collision. Log an error and continue.
|
||||
return fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket. Use one of these IP addresses %v to access the bucket", b[index].Name, globalDomainIPs.ToSlice())
|
||||
}
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
|
||||
for _, err := range g.Wait() {
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetBucketLocationHandler - GET Bucket location.
|
||||
// -------------------------
|
||||
// This operation returns bucket location.
|
||||
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "GetBucketLocation")
|
||||
ctx := newContext(r, w, "GetBucketLocation")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -55,12 +102,6 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
||||
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
|
||||
if api.CacheAPI() != nil {
|
||||
getBucketInfo = api.CacheAPI().GetBucketInfo
|
||||
@@ -93,7 +134,7 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
||||
// uploads in the response.
|
||||
//
|
||||
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "ListMultipartUploads")
|
||||
ctx := newContext(r, w, "ListMultipartUploads")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -140,7 +181,7 @@ func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter,
|
||||
// This implementation of the GET operation returns a list of all buckets
|
||||
// owned by the authenticated sender of the request.
|
||||
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "ListBuckets")
|
||||
ctx := newContext(r, w, "ListBuckets")
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
@@ -157,12 +198,33 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
writeErrorResponse(w, s3Error, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Invoke the list buckets.
|
||||
bucketsInfo, err := listBuckets(ctx)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
// If etcd, dns federation configured list buckets from etcd.
|
||||
var bucketsInfo []BucketInfo
|
||||
if globalDNSConfig != nil {
|
||||
dnsBuckets, err := globalDNSConfig.List()
|
||||
if err != nil && err != dns.ErrNoEntriesFound {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
bucketSet := set.NewStringSet()
|
||||
for _, dnsRecord := range dnsBuckets {
|
||||
if bucketSet.Contains(dnsRecord.Key) {
|
||||
continue
|
||||
}
|
||||
bucketsInfo = append(bucketsInfo, BucketInfo{
|
||||
Name: strings.Trim(dnsRecord.Key, slashSeparator),
|
||||
Created: dnsRecord.CreationDate,
|
||||
})
|
||||
bucketSet.Add(dnsRecord.Key)
|
||||
}
|
||||
} else {
|
||||
// Invoke the list buckets.
|
||||
var err error
|
||||
bucketsInfo, err = listBuckets(ctx)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate response.
|
||||
@@ -175,7 +237,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
|
||||
// DeleteMultipleObjectsHandler - deletes multiple objects.
|
||||
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "DeleteMultipleObjects")
|
||||
ctx := newContext(r, w, "DeleteMultipleObjects")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -211,7 +273,13 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
}
|
||||
|
||||
// Allocate incoming content length bytes.
|
||||
deleteXMLBytes := make([]byte, r.ContentLength)
|
||||
var deleteXMLBytes []byte
|
||||
const maxBodySize = 2 * 1000 * 1024 // The max. XML contains 1000 object names (each at most 1024 bytes long) + XML overhead
|
||||
if r.ContentLength > maxBodySize { // Only allocated memory for at most 1000 objects
|
||||
deleteXMLBytes = make([]byte, maxBodySize)
|
||||
} else {
|
||||
deleteXMLBytes = make([]byte, r.ContentLength)
|
||||
}
|
||||
|
||||
// Read incoming body XML bytes.
|
||||
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
|
||||
@@ -236,34 +304,24 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
var wg = &sync.WaitGroup{} // Allocate a new wait group.
|
||||
var dErrs = make([]error, len(deleteObjects.Objects))
|
||||
|
||||
// Delete all requested objects in parallel.
|
||||
for index, object := range deleteObjects.Objects {
|
||||
wg.Add(1)
|
||||
go func(i int, obj ObjectIdentifier) {
|
||||
defer wg.Done()
|
||||
// If the request is denied access, each item
|
||||
// should be marked as 'AccessDenied'
|
||||
if s3Error == ErrAccessDenied {
|
||||
dErrs[i] = PrefixAccessDenied{
|
||||
Bucket: bucket,
|
||||
Object: obj.ObjectName,
|
||||
}
|
||||
return
|
||||
}
|
||||
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)
|
||||
deleteObject := objectAPI.DeleteObject
|
||||
if api.CacheAPI() != nil {
|
||||
deleteObject = api.CacheAPI().DeleteObject
|
||||
}
|
||||
|
||||
var dErrs = make([]error, len(deleteObjects.Objects))
|
||||
for index, object := range deleteObjects.Objects {
|
||||
// If the request is denied access, each item
|
||||
// should be marked as 'AccessDenied'
|
||||
if s3Error == ErrAccessDenied {
|
||||
dErrs[index] = PrefixAccessDenied{
|
||||
Bucket: bucket,
|
||||
Object: object.ObjectName,
|
||||
}
|
||||
continue
|
||||
}
|
||||
dErrs[index] = deleteObject(ctx, bucket, object.ObjectName)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Collect deleted objects and errors if any.
|
||||
var deletedObjects []ObjectIdentifier
|
||||
@@ -298,7 +356,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
|
||||
// Get host and port from Request.RemoteAddr failing which
|
||||
// fill them with empty strings.
|
||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
host, port, err := net.SplitHostPort(handlers.GetSourceIP(r))
|
||||
if err != nil {
|
||||
host, port = "", ""
|
||||
}
|
||||
@@ -323,7 +381,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
// ----------
|
||||
// This implementation of the PUT operation creates a new bucket for authenticated request
|
||||
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "PutBucket")
|
||||
ctx := newContext(r, w, "PutBucket")
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
@@ -353,12 +411,33 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
||||
if err := bucketLock.GetLock(globalObjectTimeout); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
if globalDNSConfig != nil {
|
||||
if _, err := globalDNSConfig.Get(bucket); err != nil {
|
||||
if err == dns.ErrNoEntriesFound {
|
||||
// Proceed to creating a bucket.
|
||||
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalDNSConfig.Put(bucket); err != nil {
|
||||
objectAPI.DeleteBucket(ctx, bucket)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure to add Location information here only for bucket
|
||||
w.Header().Set("Location", getObjectLocation(r, globalDomainName, bucket, ""))
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
return
|
||||
}
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
|
||||
}
|
||||
writeErrorResponse(w, ErrBucketAlreadyOwnedByYou, r.URL)
|
||||
return
|
||||
}
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
// Proceed to creating a bucket.
|
||||
err := objectAPI.MakeBucketWithLocation(ctx, bucket, location)
|
||||
@@ -378,7 +457,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
// This implementation of the POST operation handles object creation with a specified
|
||||
// signature policy in multipart/form-data
|
||||
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "PostPolicyBucket")
|
||||
ctx := newContext(r, w, "PostPolicyBucket")
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
@@ -503,7 +582,8 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
// 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 {
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
@@ -525,7 +605,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
reader, err = newEncryptReader(hashReader, key, metadata)
|
||||
reader, err = newEncryptReader(hashReader, key, bucket, object, metadata)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -550,7 +630,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
w.Header().Set("Location", location)
|
||||
|
||||
// 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 {
|
||||
host, port = "", ""
|
||||
}
|
||||
@@ -597,7 +677,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
// have permission to access it. Otherwise, the operation might
|
||||
// return responses such as 404 Not Found and 403 Forbidden.
|
||||
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "HeadBucket")
|
||||
ctx := newContext(r, w, "HeadBucket")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -627,7 +707,7 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// DeleteBucketHandler - Delete bucket
|
||||
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "DeleteBucket")
|
||||
ctx := newContext(r, w, "DeleteBucket")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -655,9 +735,15 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
|
||||
globalNotificationSys.RemoveNotification(bucket)
|
||||
globalPolicySys.Remove(bucket)
|
||||
for nerr := range globalNotificationSys.DeleteBucket(bucket) {
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
globalNotificationSys.DeleteBucket(ctx, bucket)
|
||||
|
||||
if globalDNSConfig != nil {
|
||||
if err := globalDNSConfig.Delete(bucket); err != nil {
|
||||
// Deleting DNS entry failed, attempt to create the bucket again.
|
||||
objectAPI.MakeBucketWithLocation(ctx, bucket, "")
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
|
||||
@@ -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.
|
||||
// It returns empty configuration if its not set.
|
||||
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "GetBucketNotification")
|
||||
ctx := newContext(r, w, "GetBucketNotification")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucketName := vars["bucket"]
|
||||
@@ -94,7 +94,7 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
||||
// PutBucketNotificationHandler - This HTTP handler stores given notification configuration as per
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html.
|
||||
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "PutBucketNotification")
|
||||
ctx := newContext(r, w, "PutBucketNotification")
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
@@ -146,10 +146,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||
|
||||
rulesMap := config.ToRulesMap()
|
||||
globalNotificationSys.AddRulesMap(bucketName, rulesMap)
|
||||
for nerr := range globalNotificationSys.PutBucketNotification(bucketName, rulesMap) {
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
globalNotificationSys.PutBucketNotification(ctx, bucketName, rulesMap)
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
@@ -157,7 +154,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
||||
// 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.
|
||||
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "ListenBucketNotification")
|
||||
ctx := newContext(r, w, "ListenBucketNotification")
|
||||
|
||||
// Validate if bucket exists.
|
||||
objAPI := api.ObjectAPI()
|
||||
@@ -224,15 +221,21 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
return
|
||||
}
|
||||
|
||||
host, e := xnet.ParseHost(r.RemoteAddr)
|
||||
logger.CriticalIf(ctx, e)
|
||||
host, err := xnet.ParseHost(r.RemoteAddr)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
target, e := target.NewHTTPClientTarget(*host, w)
|
||||
logger.CriticalIf(ctx, e)
|
||||
target, err := target.NewHTTPClientTarget(*host, w)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
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.LogIf(ctx, err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
@@ -241,25 +244,24 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
defer globalNotificationSys.RemoveRemoteTarget(bucketName, target.ID())
|
||||
defer globalNotificationSys.RemoveRulesMap(bucketName, rulesMap)
|
||||
|
||||
thisAddr, e := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
logger.CriticalIf(ctx, e)
|
||||
thisAddr, err := xnet.ParseHost(GetLocalPeer(globalEndpoints))
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := SaveListener(objAPI, bucketName, eventNames, pattern, target.ID(), *thisAddr); err != nil {
|
||||
if err = SaveListener(objAPI, bucketName, eventNames, pattern, target.ID(), *thisAddr); err != nil {
|
||||
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
errCh := globalNotificationSys.ListenBucketNotification(bucketName, eventNames, pattern, target.ID(), *thisAddr)
|
||||
for nerr := range errCh {
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
globalNotificationSys.ListenBucketNotification(ctx, bucketName, eventNames, pattern, target.ID(), *thisAddr)
|
||||
|
||||
<-target.DoneCh
|
||||
|
||||
if err := RemoveListener(objAPI, bucketName, target.ID(), *thisAddr); err != nil {
|
||||
if err = RemoveListener(objAPI, bucketName, target.ID(), *thisAddr); err != nil {
|
||||
logger.GetReqInfo(ctx).AppendTags("target", target.ID().Name)
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
|
||||
@@ -38,7 +38,7 @@ const (
|
||||
// PutBucketPolicyHandler - This HTTP handler stores given bucket policy configuration as per
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
||||
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "PutBucketPolicy")
|
||||
ctx := newContext(r, w, "PutBucketPolicy")
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
@@ -91,10 +91,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
}
|
||||
|
||||
globalPolicySys.Set(bucket, *bucketPolicy)
|
||||
for nerr := range globalNotificationSys.SetBucketPolicy(bucket, bucketPolicy) {
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
globalNotificationSys.SetBucketPolicy(ctx, bucket, bucketPolicy)
|
||||
|
||||
// Success.
|
||||
writeSuccessNoContent(w)
|
||||
@@ -102,7 +99,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
|
||||
// DeleteBucketPolicyHandler - This HTTP handler removes bucket policy configuration.
|
||||
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "DeleteBucketPolicy")
|
||||
ctx := newContext(r, w, "DeleteBucketPolicy")
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
@@ -130,10 +127,7 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
globalPolicySys.Remove(bucket)
|
||||
for nerr := range globalNotificationSys.RemoveBucketPolicy(bucket) {
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", nerr.Host.Name)
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
globalNotificationSys.RemoveBucketPolicy(ctx, bucket)
|
||||
|
||||
// Success.
|
||||
writeSuccessNoContent(w)
|
||||
@@ -141,7 +135,7 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
||||
|
||||
// GetBucketPolicyHandler - This HTTP handler returns bucket policy configuration.
|
||||
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "GetBucketPolicy")
|
||||
ctx := newContext(r, w, "GetBucketPolicy")
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
|
||||
@@ -437,11 +437,13 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
||||
|
||||
if recV4.Code != testCase.expectedRespStatus {
|
||||
// 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 {
|
||||
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 {
|
||||
t.Fatalf("unexpected error. %v", err)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ var (
|
||||
// GOPATH - GOPATH value at the time of build.
|
||||
GOPATH = ""
|
||||
|
||||
// GOROOT - GOROOT value at the time of build.
|
||||
GOROOT = ""
|
||||
|
||||
// Go get development tag.
|
||||
goGetTag = "DEVELOPMENT.GOGET"
|
||||
|
||||
|
||||
+5
-4
@@ -86,10 +86,11 @@ func getRootCAs(certsCAsDir string) (*x509.CertPool, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rootCAs, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
// In some systems like Windows, system cert pool is not supported.
|
||||
// Hence we create a new cert pool.
|
||||
rootCAs, _ := x509.SystemCertPool()
|
||||
if rootCAs == nil {
|
||||
// In some systems (like Windows) system cert pool is
|
||||
// not supported or no certificates are present on the
|
||||
// system - so we create a new cert pool.
|
||||
rootCAs = x509.NewCertPool()
|
||||
}
|
||||
|
||||
|
||||
+83
-1
@@ -17,16 +17,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
)
|
||||
|
||||
// Check for updates and print a notification message
|
||||
@@ -41,17 +48,53 @@ func checkUpdate(mode string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and load config from remote etcd or local config directory
|
||||
func initConfig() {
|
||||
// Config file does not exist, we create it fresh and return upon success.
|
||||
if globalEtcdClient != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
resp, err := globalEtcdClient.Get(ctx, getConfigFile())
|
||||
cancel()
|
||||
// This means there are no entries in etcd with config file
|
||||
// So create a new config
|
||||
if err == nil && resp.Count == 0 {
|
||||
logger.FatalIf(newConfig(), "Unable to initialize minio config for the first time.")
|
||||
logger.Info("Created minio configuration file successfully at %v", globalEtcdClient.Endpoints())
|
||||
} else {
|
||||
// This means there is an entry in etcd, update it if required and proceed
|
||||
if err == nil && resp.Count > 0 {
|
||||
logger.FatalIf(migrateConfig(), "Config migration failed.")
|
||||
logger.FatalIf(loadConfig(), "Unable to load config version: '%s'.", serverConfigVersion)
|
||||
} else {
|
||||
logger.FatalIf(err, "Unable to load config version: '%s'.", serverConfigVersion)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if isFile(getConfigFile()) {
|
||||
logger.FatalIf(migrateConfig(), "Config migration failed")
|
||||
logger.FatalIf(loadConfig(), "Unable to load the configuration file")
|
||||
} 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())
|
||||
}
|
||||
}
|
||||
|
||||
// Load logger targets based on user's configuration
|
||||
func loadLoggers() {
|
||||
if globalServerConfig.Logger.Console.Enabled {
|
||||
// Enable console logging
|
||||
logger.AddTarget(logger.NewConsole())
|
||||
}
|
||||
for _, l := range globalServerConfig.Logger.HTTP {
|
||||
if l.Enabled {
|
||||
// Enable http logging
|
||||
logger.AddTarget(logger.NewHTTP(l.Endpoint, NewCustomHTTPTransport()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
|
||||
var configDir string
|
||||
@@ -123,8 +166,37 @@ func handleCommonEnvVars() {
|
||||
logger.FatalIf(err, "error opening file %s", traceFile)
|
||||
}
|
||||
|
||||
etcdEndpointsEnv, ok := os.LookupEnv("MINIO_ETCD_ENDPOINTS")
|
||||
if ok {
|
||||
etcdEndpoints := strings.Split(etcdEndpointsEnv, ",")
|
||||
var err error
|
||||
globalEtcdClient, err = etcd.New(etcd.Config{
|
||||
Endpoints: etcdEndpoints,
|
||||
DialTimeout: defaultDialTimeout,
|
||||
DialKeepAliveTime: defaultDialKeepAlive,
|
||||
})
|
||||
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
|
||||
}
|
||||
|
||||
globalDomainName, globalIsEnvDomainName = os.LookupEnv("MINIO_DOMAIN")
|
||||
|
||||
minioEndpointsEnv, ok := os.LookupEnv("MINIO_PUBLIC_IPS")
|
||||
if ok {
|
||||
minioEndpoints := strings.Split(minioEndpointsEnv, ",")
|
||||
globalDomainIPs = set.NewStringSet()
|
||||
for i, ip := range minioEndpoints {
|
||||
if net.ParseIP(ip) == nil {
|
||||
logger.FatalIf(errInvalidArgument, "Unable to initialize Minio server with invalid MINIO_PUBLIC_IPS[%d]: %s", i, ip)
|
||||
}
|
||||
globalDomainIPs.Add(ip)
|
||||
}
|
||||
}
|
||||
if globalDomainName != "" && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
|
||||
var err error
|
||||
globalDNSConfig, err = dns.NewCoreDNS(globalDomainName, globalDomainIPs, globalMinioPort, globalEtcdClient)
|
||||
logger.FatalIf(err, "Unable to initialize DNS config for %s.", globalDomainName)
|
||||
}
|
||||
|
||||
if drives := os.Getenv("MINIO_CACHE_DRIVES"); drives != "" {
|
||||
driveList, err := parseCacheDrives(strings.Split(drives, cacheEnvDelimiter))
|
||||
if err != nil {
|
||||
@@ -150,6 +222,16 @@ func handleCommonEnvVars() {
|
||||
globalCacheExpiry = expiry
|
||||
}
|
||||
|
||||
if maxUseStr := os.Getenv("MINIO_CACHE_MAXUSE"); maxUseStr != "" {
|
||||
maxUse, err := strconv.Atoi(maxUseStr)
|
||||
if err != nil {
|
||||
logger.Fatal(uiErrInvalidCacheMaxUse(err), "Unable to parse MINIO_CACHE_MAXUSE value (`%s`)", maxUseStr)
|
||||
}
|
||||
// maxUse should be a valid percentage.
|
||||
if maxUse > 0 && maxUse <= 100 {
|
||||
globalCacheMaxUse = maxUse
|
||||
}
|
||||
}
|
||||
// In place update is true by default if the MINIO_UPDATE is not set
|
||||
// or is not set to 'off', if MINIO_UPDATE is set to 'off' then
|
||||
// in-place update is off.
|
||||
|
||||
+174
-38
@@ -17,11 +17,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
@@ -39,9 +41,9 @@ import (
|
||||
// 6. Make changes in config-current_test.go for any test change
|
||||
|
||||
// Config version
|
||||
const serverConfigVersion = "25"
|
||||
const serverConfigVersion = "27"
|
||||
|
||||
type serverConfig = serverConfigV25
|
||||
type serverConfig = serverConfigV27
|
||||
|
||||
var (
|
||||
// globalServerConfig server config.
|
||||
@@ -116,10 +118,11 @@ func (s *serverConfig) GetWorm() bool {
|
||||
}
|
||||
|
||||
// SetCacheConfig sets the current cache config
|
||||
func (s *serverConfig) SetCacheConfig(drives, exclude []string, expiry int) {
|
||||
func (s *serverConfig) SetCacheConfig(drives, exclude []string, expiry int, maxuse int) {
|
||||
s.Cache.Drives = drives
|
||||
s.Cache.Exclude = exclude
|
||||
s.Cache.Expiry = expiry
|
||||
s.Cache.MaxUse = maxuse
|
||||
}
|
||||
|
||||
// GetCacheConfig gets the current cache config
|
||||
@@ -127,10 +130,97 @@ func (s *serverConfig) GetCacheConfig() CacheConfig {
|
||||
return s.Cache
|
||||
}
|
||||
|
||||
// Save config.
|
||||
func (s *serverConfig) Save() error {
|
||||
// Save config file.
|
||||
return quick.Save(getConfigFile(), s)
|
||||
func (s *serverConfig) Validate() error {
|
||||
if s.Version != serverConfigVersion {
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Returns the string describing a difference with the given
|
||||
@@ -170,6 +260,8 @@ func (s *serverConfig) ConfigDiff(t *serverConfig) string {
|
||||
return "MySQL Notification configuration differs"
|
||||
case !reflect.DeepEqual(s.Notify.MQTT, t.Notify.MQTT):
|
||||
return "MQTT Notification configuration differs"
|
||||
case !reflect.DeepEqual(s.Logger, t.Logger):
|
||||
return "Logger configuration differs"
|
||||
case reflect.DeepEqual(s, t):
|
||||
return ""
|
||||
default:
|
||||
@@ -196,6 +288,7 @@ func newServerConfig() *serverConfig {
|
||||
Drives: []string{},
|
||||
Exclude: []string{},
|
||||
Expiry: globalCacheExpiry,
|
||||
MaxUse: globalCacheMaxUse,
|
||||
},
|
||||
Notify: notifier{},
|
||||
}
|
||||
@@ -223,6 +316,14 @@ func newServerConfig() *serverConfig {
|
||||
srvCfg.Cache.Drives = make([]string, 0)
|
||||
srvCfg.Cache.Exclude = make([]string, 0)
|
||||
srvCfg.Cache.Expiry = globalCacheExpiry
|
||||
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
|
||||
}
|
||||
|
||||
@@ -230,7 +331,10 @@ func newServerConfig() *serverConfig {
|
||||
// found, otherwise use default parameters
|
||||
func newConfig() error {
|
||||
// Initialize server config.
|
||||
srvCfg := newServerConfig()
|
||||
srvCfg, err := newQuickConfig(newServerConfig())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If env is set override the credentials from config file.
|
||||
if globalIsEnvCreds {
|
||||
@@ -258,7 +362,7 @@ func newConfig() error {
|
||||
}
|
||||
|
||||
if globalIsDiskCacheEnabled {
|
||||
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry)
|
||||
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
||||
}
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
@@ -269,7 +373,19 @@ func newConfig() error {
|
||||
globalServerConfigMu.Unlock()
|
||||
|
||||
// Save config into file.
|
||||
return globalServerConfig.Save()
|
||||
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
|
||||
@@ -279,19 +395,19 @@ func getValidConfig() (*serverConfig, error) {
|
||||
Browser: true,
|
||||
}
|
||||
|
||||
if _, err := quick.Load(getConfigFile(), srvCfg); err != nil {
|
||||
var err error
|
||||
srvCfg, err = newQuickConfig(srvCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if srvCfg.Version != serverConfigVersion {
|
||||
return nil, fmt.Errorf("configuration version mismatch. Expected: ‘%s’, Got: ‘%s’", serverConfigVersion, srvCfg.Version)
|
||||
configFile := getConfigFile()
|
||||
if err = srvCfg.Load(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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())
|
||||
if err = srvCfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return srvCfg, nil
|
||||
@@ -327,7 +443,7 @@ func loadConfig() error {
|
||||
}
|
||||
|
||||
if globalIsDiskCacheEnabled {
|
||||
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry)
|
||||
srvCfg.SetCacheConfig(globalCacheDrives, globalCacheExcludes, globalCacheExpiry, globalCacheMaxUse)
|
||||
}
|
||||
|
||||
// hold the mutex lock before a new config is assigned.
|
||||
@@ -356,6 +472,7 @@ func loadConfig() error {
|
||||
globalCacheDrives = cacheConf.Drives
|
||||
globalCacheExcludes = cacheConf.Exclude
|
||||
globalCacheExpiry = cacheConf.Expiry
|
||||
globalCacheMaxUse = cacheConf.MaxUse
|
||||
}
|
||||
globalServerConfigMu.Unlock()
|
||||
|
||||
@@ -367,17 +484,19 @@ func loadConfig() error {
|
||||
// * Add a new target in pkg/event/target package.
|
||||
// * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>.
|
||||
// * 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()
|
||||
|
||||
for id, args := range config.Notify.AMQP {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewAMQPTarget(id, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,10 +505,14 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewElasticsearchTarget(id, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
|
||||
}
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,10 +521,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewKafkaTarget(id, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,10 +535,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewMQTTTarget(id, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,10 +549,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewMySQLTarget(id, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,10 +563,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewNATSTarget(id, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,10 +577,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewPostgreSQLTarget(id, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,10 +591,12 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
||||
if args.Enable {
|
||||
newTarget, err := target.NewRedisTarget(id, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,10 +605,11 @@ func getNotificationTargets(config *serverConfig) (*event.TargetList, error) {
|
||||
if args.Enable {
|
||||
newTarget := target.NewWebhookTarget(id, args)
|
||||
if err := targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return targetList, nil
|
||||
return targetList
|
||||
}
|
||||
|
||||
+26
-14
@@ -50,7 +50,7 @@ func TestServerConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
// Attempt to save.
|
||||
if err := globalServerConfig.Save(); err != nil {
|
||||
if err := globalServerConfig.Save(getConfigFile()); err != nil {
|
||||
t.Fatalf("Unable to save updated config file %s", err)
|
||||
}
|
||||
|
||||
@@ -174,55 +174,55 @@ func TestValidateConfig(t *testing.T) {
|
||||
{`{"version": "` + v + `", "browser": "on", "browser": "on", "region":"us-east-1", "credential" : {"accessKey":"minio", "secretKey":"minio123"}}`, false},
|
||||
|
||||
// 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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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
|
||||
{`{"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 {
|
||||
@@ -319,6 +319,18 @@ func TestConfigDiff(t *testing.T) {
|
||||
&serverConfig{Notify: notifier{MQTT: map[string]target.MQTTArgs{"1": {Enable: false}}}},
|
||||
"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 {
|
||||
|
||||
+212
-49
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
@@ -42,7 +43,7 @@ func migrateConfig() error {
|
||||
}
|
||||
|
||||
// Load only config version information.
|
||||
version, err := quick.GetVersion(getConfigFile())
|
||||
version, err := GetVersion(getConfigFile())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -182,6 +183,16 @@ func migrateConfig() error {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case "25":
|
||||
if err = migrateV25ToV26(); err != nil {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case "26":
|
||||
if err = migrateV26ToV27(); err != nil {
|
||||
return err
|
||||
}
|
||||
fallthrough
|
||||
case serverConfigVersion:
|
||||
// No migration needed. this always points to current version.
|
||||
err = nil
|
||||
@@ -194,8 +205,8 @@ func purgeV1() error {
|
||||
configFile := filepath.Join(getConfigDir(), "fsUsers.json")
|
||||
|
||||
cv1 := &configV1{}
|
||||
_, err := quick.Load(configFile, cv1)
|
||||
if os.IsNotExist(err) {
|
||||
_, err := Load(configFile, cv1)
|
||||
if os.IsNotExist(err) || err == dns.ErrNoEntriesFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config version ‘1’. %v", err)
|
||||
@@ -215,7 +226,7 @@ func migrateV2ToV3() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv2 := &configV2{}
|
||||
_, err := quick.Load(configFile, cv2)
|
||||
_, err := Load(configFile, cv2)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -259,7 +270,7 @@ func migrateV2ToV3() error {
|
||||
}
|
||||
srvConfig.Logger.Syslog = slogger
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv2.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -274,7 +285,7 @@ func migrateV3ToV4() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv3 := &configV3{}
|
||||
_, err := quick.Load(configFile, cv3)
|
||||
_, err := Load(configFile, cv3)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -297,7 +308,7 @@ func migrateV3ToV4() error {
|
||||
srvConfig.Logger.File = cv3.Logger.File
|
||||
srvConfig.Logger.Syslog = cv3.Logger.Syslog
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv3.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -312,7 +323,7 @@ func migrateV4ToV5() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv4 := &configV4{}
|
||||
_, err := quick.Load(configFile, cv4)
|
||||
_, err := Load(configFile, cv4)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -338,7 +349,7 @@ func migrateV4ToV5() error {
|
||||
srvConfig.Logger.ElasticSearch.Enable = false
|
||||
srvConfig.Logger.Redis.Enable = false
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv4.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -353,7 +364,7 @@ func migrateV5ToV6() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv5 := &configV5{}
|
||||
_, err := quick.Load(configFile, cv5)
|
||||
_, err := Load(configFile, cv5)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -427,7 +438,7 @@ func migrateV5ToV6() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv5.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -442,7 +453,7 @@ func migrateV6ToV7() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv6 := &configV6{}
|
||||
_, err := quick.Load(configFile, cv6)
|
||||
_, err := Load(configFile, cv6)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -483,7 +494,7 @@ func migrateV6ToV7() error {
|
||||
srvConfig.Notify.Redis = cv6.Notify.Redis
|
||||
}
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv6.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -498,7 +509,7 @@ func migrateV7ToV8() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv7 := &serverConfigV7{}
|
||||
_, err := quick.Load(configFile, cv7)
|
||||
_, err := Load(configFile, cv7)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -546,7 +557,7 @@ func migrateV7ToV8() error {
|
||||
srvConfig.Notify.Redis = cv7.Notify.Redis
|
||||
}
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv7.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -560,7 +571,7 @@ func migrateV8ToV9() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv8 := &serverConfigV8{}
|
||||
_, err := quick.Load(configFile, cv8)
|
||||
_, err := Load(configFile, cv8)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -616,7 +627,7 @@ func migrateV8ToV9() error {
|
||||
srvConfig.Notify.PostgreSQL = cv8.Notify.PostgreSQL
|
||||
}
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv8.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -630,7 +641,7 @@ func migrateV9ToV10() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv9 := &serverConfigV9{}
|
||||
_, err := quick.Load(configFile, cv9)
|
||||
_, err := Load(configFile, cv9)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -684,7 +695,7 @@ func migrateV9ToV10() error {
|
||||
srvConfig.Notify.PostgreSQL = cv9.Notify.PostgreSQL
|
||||
}
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv9.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -698,7 +709,7 @@ func migrateV10ToV11() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv10 := &serverConfigV10{}
|
||||
_, err := quick.Load(configFile, cv10)
|
||||
_, err := Load(configFile, cv10)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -755,7 +766,7 @@ func migrateV10ToV11() error {
|
||||
srvConfig.Notify.Kafka = make(map[string]target.KafkaArgs)
|
||||
srvConfig.Notify.Kafka["1"] = target.KafkaArgs{}
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv10.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -769,7 +780,7 @@ func migrateV11ToV12() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv11 := &serverConfigV11{}
|
||||
_, err := quick.Load(configFile, cv11)
|
||||
_, err := Load(configFile, cv11)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -853,7 +864,7 @@ func migrateV11ToV12() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv11.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -866,7 +877,7 @@ func migrateV12ToV13() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv12 := &serverConfigV12{}
|
||||
_, err := quick.Load(configFile, cv12)
|
||||
_, err := Load(configFile, cv12)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -933,7 +944,7 @@ func migrateV12ToV13() error {
|
||||
srvConfig.Notify.Webhook = make(map[string]target.WebhookArgs)
|
||||
srvConfig.Notify.Webhook["1"] = target.WebhookArgs{}
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv12.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -946,7 +957,7 @@ func migrateV13ToV14() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv13 := &serverConfigV13{}
|
||||
_, err := quick.Load(configFile, cv13)
|
||||
_, err := Load(configFile, cv13)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1018,7 +1029,7 @@ func migrateV13ToV14() error {
|
||||
// Set the new browser parameter to true by default
|
||||
srvConfig.Browser = true
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv13.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1031,7 +1042,7 @@ func migrateV14ToV15() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv14 := &serverConfigV14{}
|
||||
_, err := quick.Load(configFile, cv14)
|
||||
_, err := Load(configFile, cv14)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1107,7 +1118,7 @@ func migrateV14ToV15() error {
|
||||
// Load browser config from existing config in the file.
|
||||
srvConfig.Browser = cv14.Browser
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv14.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1121,7 +1132,7 @@ func migrateV15ToV16() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv15 := &serverConfigV15{}
|
||||
_, err := quick.Load(configFile, cv15)
|
||||
_, err := Load(configFile, cv15)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1197,7 +1208,7 @@ func migrateV15ToV16() error {
|
||||
// Load browser config from existing config in the file.
|
||||
srvConfig.Browser = cv15.Browser
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv15.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1211,7 +1222,7 @@ func migrateV16ToV17() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv16 := &serverConfigV16{}
|
||||
_, err := quick.Load(configFile, cv16)
|
||||
_, err := Load(configFile, cv16)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1318,7 +1329,7 @@ func migrateV16ToV17() error {
|
||||
// Load browser config from existing config in the file.
|
||||
srvConfig.Browser = cv16.Browser
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv16.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1332,7 +1343,7 @@ func migrateV17ToV18() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv17 := &serverConfigV17{}
|
||||
_, err := quick.Load(configFile, cv17)
|
||||
_, err := Load(configFile, cv17)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1422,7 +1433,7 @@ func migrateV17ToV18() error {
|
||||
// Load browser config from existing config in the file.
|
||||
srvConfig.Browser = cv17.Browser
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv17.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1434,7 +1445,7 @@ func migrateV18ToV19() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv18 := &serverConfigV18{}
|
||||
_, err := quick.Load(configFile, cv18)
|
||||
_, err := Load(configFile, cv18)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1528,7 +1539,7 @@ func migrateV18ToV19() error {
|
||||
// Load browser config from existing config in the file.
|
||||
srvConfig.Browser = cv18.Browser
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv18.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1540,7 +1551,7 @@ func migrateV19ToV20() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv19 := &serverConfigV19{}
|
||||
_, err := quick.Load(configFile, cv19)
|
||||
_, err := Load(configFile, cv19)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1633,7 +1644,7 @@ func migrateV19ToV20() error {
|
||||
// Load browser config from existing config in the file.
|
||||
srvConfig.Browser = cv19.Browser
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv19.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1645,7 +1656,7 @@ func migrateV20ToV21() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv20 := &serverConfigV20{}
|
||||
_, err := quick.Load(configFile, cv20)
|
||||
_, err := Load(configFile, cv20)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1737,7 +1748,7 @@ func migrateV20ToV21() error {
|
||||
// Load domain config from existing config in the file.
|
||||
srvConfig.Domain = cv20.Domain
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv20.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1749,7 +1760,7 @@ func migrateV21ToV22() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv21 := &serverConfigV21{}
|
||||
_, err := quick.Load(configFile, cv21)
|
||||
_, err := Load(configFile, cv21)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1841,7 +1852,7 @@ func migrateV21ToV22() error {
|
||||
// Load domain config from existing config in the file.
|
||||
srvConfig.Domain = cv21.Domain
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv21.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1853,7 +1864,7 @@ func migrateV22ToV23() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv22 := &serverConfigV22{}
|
||||
_, err := quick.Load(configFile, cv22)
|
||||
_, err := Load(configFile, cv22)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -1954,7 +1965,7 @@ func migrateV22ToV23() error {
|
||||
srvConfig.Cache.Exclude = []string{}
|
||||
srvConfig.Cache.Expiry = globalCacheExpiry
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = Save(configFile, srvConfig); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv22.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -1966,7 +1977,7 @@ func migrateV23ToV24() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv23 := &serverConfigV23{}
|
||||
_, err := quick.Load(configFile, cv23)
|
||||
_, err := quick.LoadConfig(configFile, globalEtcdClient, cv23)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -2067,7 +2078,7 @@ func migrateV23ToV24() error {
|
||||
srvConfig.Cache.Exclude = cv23.Cache.Exclude
|
||||
srvConfig.Cache.Expiry = cv23.Cache.Expiry
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = quick.SaveConfig(srvConfig, configFile, globalEtcdClient); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv23.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
@@ -2079,7 +2090,7 @@ func migrateV24ToV25() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv24 := &serverConfigV24{}
|
||||
_, err := quick.Load(configFile, cv24)
|
||||
_, err := quick.LoadConfig(configFile, globalEtcdClient, cv24)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -2185,10 +2196,162 @@ func migrateV24ToV25() error {
|
||||
srvConfig.Cache.Exclude = cv24.Cache.Exclude
|
||||
srvConfig.Cache.Expiry = cv24.Cache.Expiry
|
||||
|
||||
if err = quick.Save(configFile, srvConfig); err != nil {
|
||||
if err = quick.SaveConfig(srvConfig, configFile, globalEtcdClient); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv24.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, cv24.Version, srvConfig.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV25ToV26() error {
|
||||
configFile := getConfigFile()
|
||||
|
||||
cv25 := &serverConfigV25{}
|
||||
_, err := quick.LoadConfig(configFile, globalEtcdClient, cv25)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config version ‘25’. %v", err)
|
||||
}
|
||||
if cv25.Version != "25" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy over fields from V25 into V26 config struct
|
||||
srvConfig := &serverConfigV26{
|
||||
Notify: notifier{},
|
||||
}
|
||||
srvConfig.Version = "26"
|
||||
srvConfig.Credential = cv25.Credential
|
||||
srvConfig.Region = cv25.Region
|
||||
if srvConfig.Region == "" {
|
||||
// Region needs to be set for AWS Signature Version 4.
|
||||
srvConfig.Region = globalMinioDefaultRegion
|
||||
}
|
||||
|
||||
if len(cv25.Notify.AMQP) == 0 {
|
||||
srvConfig.Notify.AMQP = make(map[string]target.AMQPArgs)
|
||||
srvConfig.Notify.AMQP["1"] = target.AMQPArgs{}
|
||||
} else {
|
||||
srvConfig.Notify.AMQP = cv25.Notify.AMQP
|
||||
}
|
||||
if len(cv25.Notify.Elasticsearch) == 0 {
|
||||
srvConfig.Notify.Elasticsearch = make(map[string]target.ElasticsearchArgs)
|
||||
srvConfig.Notify.Elasticsearch["1"] = target.ElasticsearchArgs{
|
||||
Format: event.NamespaceFormat,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.Elasticsearch = cv25.Notify.Elasticsearch
|
||||
}
|
||||
if len(cv25.Notify.Redis) == 0 {
|
||||
srvConfig.Notify.Redis = make(map[string]target.RedisArgs)
|
||||
srvConfig.Notify.Redis["1"] = target.RedisArgs{
|
||||
Format: event.NamespaceFormat,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.Redis = cv25.Notify.Redis
|
||||
}
|
||||
if len(cv25.Notify.PostgreSQL) == 0 {
|
||||
srvConfig.Notify.PostgreSQL = make(map[string]target.PostgreSQLArgs)
|
||||
srvConfig.Notify.PostgreSQL["1"] = target.PostgreSQLArgs{
|
||||
Format: event.NamespaceFormat,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.PostgreSQL = cv25.Notify.PostgreSQL
|
||||
}
|
||||
if len(cv25.Notify.Kafka) == 0 {
|
||||
srvConfig.Notify.Kafka = make(map[string]target.KafkaArgs)
|
||||
srvConfig.Notify.Kafka["1"] = target.KafkaArgs{}
|
||||
} else {
|
||||
srvConfig.Notify.Kafka = cv25.Notify.Kafka
|
||||
}
|
||||
if len(cv25.Notify.NATS) == 0 {
|
||||
srvConfig.Notify.NATS = make(map[string]target.NATSArgs)
|
||||
srvConfig.Notify.NATS["1"] = target.NATSArgs{}
|
||||
} else {
|
||||
srvConfig.Notify.NATS = cv25.Notify.NATS
|
||||
}
|
||||
if len(cv25.Notify.Webhook) == 0 {
|
||||
srvConfig.Notify.Webhook = make(map[string]target.WebhookArgs)
|
||||
srvConfig.Notify.Webhook["1"] = target.WebhookArgs{}
|
||||
} else {
|
||||
srvConfig.Notify.Webhook = cv25.Notify.Webhook
|
||||
}
|
||||
if len(cv25.Notify.MySQL) == 0 {
|
||||
srvConfig.Notify.MySQL = make(map[string]target.MySQLArgs)
|
||||
srvConfig.Notify.MySQL["1"] = target.MySQLArgs{
|
||||
Format: event.NamespaceFormat,
|
||||
}
|
||||
} else {
|
||||
srvConfig.Notify.MySQL = cv25.Notify.MySQL
|
||||
}
|
||||
|
||||
if len(cv25.Notify.MQTT) == 0 {
|
||||
srvConfig.Notify.MQTT = make(map[string]target.MQTTArgs)
|
||||
srvConfig.Notify.MQTT["1"] = target.MQTTArgs{}
|
||||
} else {
|
||||
srvConfig.Notify.MQTT = cv25.Notify.MQTT
|
||||
}
|
||||
|
||||
// Load browser config from existing config in the file.
|
||||
srvConfig.Browser = cv25.Browser
|
||||
|
||||
// Load worm config from existing config in the file.
|
||||
srvConfig.Worm = cv25.Worm
|
||||
|
||||
// Load domain config from existing config in the file.
|
||||
srvConfig.Domain = cv25.Domain
|
||||
|
||||
// Load storage class config from existing storage class config in the file.
|
||||
srvConfig.StorageClass.RRS = cv25.StorageClass.RRS
|
||||
srvConfig.StorageClass.Standard = cv25.StorageClass.Standard
|
||||
|
||||
// Load cache config from existing cache config in the file.
|
||||
srvConfig.Cache.Drives = cv25.Cache.Drives
|
||||
srvConfig.Cache.Exclude = cv25.Cache.Exclude
|
||||
srvConfig.Cache.Expiry = cv25.Cache.Expiry
|
||||
|
||||
// Add predefined value to new server config.
|
||||
srvConfig.Cache.MaxUse = globalCacheMaxUse
|
||||
|
||||
if err = quick.SaveConfig(srvConfig, configFile, globalEtcdClient); err != nil {
|
||||
return fmt.Errorf("Failed to migrate config from ‘%s’ to ‘%s’. %v", cv25.Version, srvConfig.Version, err)
|
||||
}
|
||||
|
||||
logger.Info(configMigrateMSGTemplate, configFile, cv25.Version, srvConfig.Version)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -134,10 +134,25 @@ func TestServerConfigMigrateInexistentConfig(t *testing.T) {
|
||||
if err := migrateV21ToV22(); err != nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// Test if a config migration from v2 to v23 is successfully done
|
||||
func TestServerConfigMigrateV2toV23(t *testing.T) {
|
||||
// Test if a config migration from v2 to v27 is successfully done
|
||||
func TestServerConfigMigrateV2toV27(t *testing.T) {
|
||||
rootPath, err := newTestConfig(globalMinioDefaultRegion)
|
||||
if err != nil {
|
||||
t.Fatalf("Init Test config failed")
|
||||
@@ -272,6 +287,18 @@ func TestServerConfigMigrateFaultyConfig(t *testing.T) {
|
||||
if err := migrateV22ToV23(); err == nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// Test if all migrate code returns error with corrupted config files
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
/////////////////// Config V1 ///////////////////
|
||||
@@ -633,6 +634,8 @@ type serverConfigV24 struct {
|
||||
// IMPORTANT NOTE: When updating this struct make sure that
|
||||
// serverConfig.ConfigDiff() is updated as necessary.
|
||||
type serverConfigV25 struct {
|
||||
quick.Config `json:"-"` // ignore interfaces
|
||||
|
||||
Version string `json:"version"`
|
||||
|
||||
// S3 API configuration.
|
||||
@@ -651,3 +654,71 @@ type serverConfigV25 struct {
|
||||
// Notification queue configuration.
|
||||
Notify notifier `json:"notify"`
|
||||
}
|
||||
|
||||
// serverConfigV26 is just like version '25', stores additionally
|
||||
// cache max use value in 'CacheConfig'.
|
||||
type serverConfigV26 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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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 implements AWS S3 related cryptographic building blocks
|
||||
// for implementing Server-Side-Encryption (SSE-S3) and Server-Side-Encryption
|
||||
// with customer provided keys (SSE-C).
|
||||
//
|
||||
// All objects are encrypted with an unique and randomly generated 'ObjectKey'.
|
||||
// The ObjectKey itself is never stored in plaintext. Instead it is only stored
|
||||
// in a sealed from. The sealed 'ObjectKey' is created by encrypting the 'ObjectKey'
|
||||
// with an unique key-encryption-key. Given the correct key-encryption-key the
|
||||
// sealed 'ObjectKey' can be unsealed and the object can be decrypted.
|
||||
//
|
||||
//
|
||||
// ## SSE-C
|
||||
//
|
||||
// SSE-C computes the key-encryption-key from the client-provided key, an
|
||||
// initialization vector (IV) and the bucket/object path.
|
||||
//
|
||||
// 1. Encrypt:
|
||||
// Input: ClientKey, bucket, object, metadata, object_data
|
||||
// - IV := Random({0,1}²⁵⁶)
|
||||
// - ObjectKey := SHA256(ClientKey || Random({0,1}²⁵⁶))
|
||||
// - KeyEncKey := HMAC-SHA256(ClientKey, IV || 'SSE-C' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
|
||||
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
|
||||
// - metadata <- IV
|
||||
// - metadata <- SealedKey
|
||||
// Output: enc_object_data, metadata
|
||||
//
|
||||
// 2. Decrypt:
|
||||
// Input: ClientKey, bucket, object, metadata, enc_object_data
|
||||
// - IV <- metadata
|
||||
// - SealedKey <- metadata
|
||||
// - KeyEncKey := HMAC-SHA256(ClientKey, IV || 'SSE-C' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
|
||||
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
|
||||
// Output: object_data
|
||||
//
|
||||
//
|
||||
// ## SSE-S3
|
||||
//
|
||||
// SSE-S3 can use either a master key or a KMS as root-of-trust.
|
||||
// The en/decryption slightly depens upon which root-of-trust is used.
|
||||
//
|
||||
// ### SSE-S3 and single master key
|
||||
//
|
||||
// The master key is used to derive unique object- and key-encryption-keys.
|
||||
// SSE-S3 with a single master key works as SSE-C where the master key is
|
||||
// used as the client-provided key.
|
||||
//
|
||||
// 1. Encrypt:
|
||||
// Input: MasterKey, bucket, object, metadata, object_data
|
||||
// - IV := Random({0,1}²⁵⁶)
|
||||
// - ObjectKey := SHA256(MasterKey || Random({0,1}²⁵⁶))
|
||||
// - KeyEncKey := HMAC-SHA256(MasterKey, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
|
||||
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
|
||||
// - metadata <- IV
|
||||
// - metadata <- SealedKey
|
||||
// Output: enc_object_data, metadata
|
||||
//
|
||||
// 2. Decrypt:
|
||||
// Input: MasterKey, bucket, object, metadata, enc_object_data
|
||||
// - IV <- metadata
|
||||
// - SealedKey <- metadata
|
||||
// - KeyEncKey := HMAC-SHA256(MasterKey, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
|
||||
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
|
||||
// Output: object_data
|
||||
//
|
||||
//
|
||||
// ### SSE-S3 and KMS
|
||||
//
|
||||
// SSE-S3 requires that the KMS provides two functions:
|
||||
// 1. Generate(KeyID) -> (Key, EncKey)
|
||||
// 2. Unseal(KeyID, EncKey) -> Key
|
||||
//
|
||||
// 1. Encrypt:
|
||||
// Input: KeyID, bucket, object, metadata, object_data
|
||||
// - Key, EncKey := Generate(KeyID)
|
||||
// - IV := Random({0,1}²⁵⁶)
|
||||
// - ObjectKey := SHA256(Key, Random({0,1}²⁵⁶))
|
||||
// - KeyEncKey := HMAC-SHA256(Key, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||
// - SealedKey := DAREv2_Enc(KeyEncKey, ObjectKey)
|
||||
// - enc_object_data := DAREv2_Enc(ObjectKey, object_data)
|
||||
// - metadata <- IV
|
||||
// - metadata <- KeyID
|
||||
// - metadata <- EncKey
|
||||
// - metadata <- SealedKey
|
||||
// Output: enc_object_data, metadata
|
||||
//
|
||||
// 2. Decrypt:
|
||||
// Input: bucket, object, metadata, enc_object_data
|
||||
// - KeyID <- metadata
|
||||
// - EncKey <- metadata
|
||||
// - IV <- metadata
|
||||
// - SealedKey <- metadata
|
||||
// - Key := Unseal(KeyID, EncKey)
|
||||
// - KeyEncKey := HMAC-SHA256(Key, IV || 'SSE-S3' || 'DAREv2-HMAC-SHA256' || bucket || '/' || object)
|
||||
// - ObjectKey := DAREv2_Dec(KeyEncKey, SealedKey)
|
||||
// - object_data := DAREv2_Dec(ObjectKey, enc_object_data)
|
||||
// Output: object_data
|
||||
//
|
||||
package crypto
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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 "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 (
|
||||
// ErrInvalidEncryptionMethod indicates that the specified SSE 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")
|
||||
)
|
||||
|
||||
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")
|
||||
)
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SSEHeader is the general AWS SSE HTTP header key.
|
||||
const SSEHeader = "X-Amz-Server-Side-Encryption"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// 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
|
||||
const SSEAlgorithmAES256 = "AES256"
|
||||
|
||||
// S3 represents AWS SSE-S3. It provides functionality to handle
|
||||
// SSE-S3 requests.
|
||||
var S3 = s3{}
|
||||
|
||||
type s3 struct{}
|
||||
|
||||
// IsRequested returns true if the HTTP headers indicates that
|
||||
// the S3 client requests SSE-S3.
|
||||
func (s3) IsRequested(h http.Header) bool {
|
||||
_, ok := h[SSEHeader]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ParseHTTP parses the SSE-S3 related HTTP headers and checks
|
||||
// whether they contain valid values.
|
||||
func (s3) ParseHTTP(h http.Header) (err error) {
|
||||
if h.Get(SSEHeader) != SSEAlgorithmAES256 {
|
||||
err = ErrInvalidEncryptionMethod
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
// 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 (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func TestS3IsRequested(t *testing.T) {
|
||||
for i, test := range s3IsRequestedTests {
|
||||
if got := S3.IsRequested(test.Header); got != test.Expected {
|
||||
t.Errorf("Test %d: Wanted %v but got %v", i, test.Expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var s3ParseTests = []struct {
|
||||
Header http.Header
|
||||
ExpectedErr error
|
||||
}{
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES256"}}, ExpectedErr: nil}, // 0
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{"AES-256"}}, ExpectedErr: ErrInvalidEncryptionMethod}, // 1
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryption": []string{""}}, ExpectedErr: ErrInvalidEncryptionMethod}, // 2
|
||||
{Header: http.Header{"X-Amz-Server-Side-Encryptio": []string{"AES256"}}, ExpectedErr: ErrInvalidEncryptionMethod}, // 3
|
||||
}
|
||||
|
||||
func TestS3Parse(t *testing.T) {
|
||||
for i, test := range s3ParseTests {
|
||||
if err := S3.ParseHTTP(test.Header); err != test.ExpectedErr {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
sha256 "github.com/minio/sha256-simd"
|
||||
"github.com/minio/sio"
|
||||
)
|
||||
|
||||
// ObjectKey is a 256 bit secret key used to encrypt the object.
|
||||
// It must never be stored in plaintext.
|
||||
type ObjectKey [32]byte
|
||||
|
||||
// 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
|
||||
// (crypto/rand) is used.
|
||||
func GenerateKey(extKey [32]byte, random io.Reader) (key ObjectKey) {
|
||||
if random == nil {
|
||||
random = rand.Reader
|
||||
}
|
||||
var nonce [32]byte
|
||||
if _, err := io.ReadFull(random, nonce[:]); err != nil {
|
||||
logger.CriticalIf(context.Background(), errOutOfEntropy)
|
||||
}
|
||||
sha := sha256.New()
|
||||
sha.Write(extKey[:])
|
||||
sha.Write(nonce[:])
|
||||
sha.Sum(key[:0])
|
||||
return key
|
||||
}
|
||||
|
||||
// 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
|
||||
// key is also cryptographically bound to the object's path (bucket/object) and the
|
||||
// domain (SSE-C or SSE-S3).
|
||||
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.Write(iv[:])
|
||||
mac.Write([]byte(domain))
|
||||
mac.Write([]byte(SealAlgorithm))
|
||||
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"))
|
||||
}
|
||||
sealedKey := SealedKey{
|
||||
IV: iv,
|
||||
Algorithm: SealAlgorithm,
|
||||
}
|
||||
copy(sealedKey.Key[:], encryptedKey.Bytes())
|
||||
return sealedKey
|
||||
}
|
||||
|
||||
// Unseal decrypts a sealed key using the 256 bit external key. Since the sealed key
|
||||
// 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.
|
||||
func (key *ObjectKey) Unseal(extKey [32]byte, sealedKey SealedKey, domain, bucket, object string) error {
|
||||
var (
|
||||
unsealConfig sio.Config
|
||||
decryptedKey bytes.Buffer
|
||||
)
|
||||
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(&decryptedKey, bytes.NewReader(sealedKey.Key[:]), unsealConfig); n != 32 || err != nil {
|
||||
return err // TODO(aead): upgrade sio to use sio.Error
|
||||
}
|
||||
copy(key[:], decryptedKey.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
// DerivePartKey derives an unique 256 bit key from an ObjectKey and the part index.
|
||||
func (key ObjectKey) DerivePartKey(id uint32) (partKey [32]byte) {
|
||||
var bin [4]byte
|
||||
binary.LittleEndian.PutUint32(bin[:], id)
|
||||
|
||||
mac := hmac.New(sha256.New, key[:])
|
||||
mac.Write(bin[:])
|
||||
mac.Sum(partKey[:0])
|
||||
return partKey
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
var shortRandom = func(limit int64) io.Reader { return io.LimitReader(rand.Reader, limit) }
|
||||
|
||||
func recoverTest(i int, shouldPass bool, t *testing.T) {
|
||||
if err := recover(); err == nil && !shouldPass {
|
||||
t.Errorf("Test %d should fail but passed successfully", i)
|
||||
} else if err != nil && shouldPass {
|
||||
t.Errorf("Test %d should pass but failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
var generateKeyTests = []struct {
|
||||
ExtKey [32]byte
|
||||
Random io.Reader
|
||||
ShouldPass bool
|
||||
}{
|
||||
{ExtKey: [32]byte{}, Random: nil, ShouldPass: true}, // 0
|
||||
{ExtKey: [32]byte{}, Random: rand.Reader, ShouldPass: true}, // 1
|
||||
{ExtKey: [32]byte{}, Random: shortRandom(32), ShouldPass: true}, // 2
|
||||
{ExtKey: [32]byte{}, Random: shortRandom(31), ShouldPass: false}, // 3
|
||||
}
|
||||
|
||||
func TestGenerateKey(t *testing.T) {
|
||||
defer func(disableLog bool) { logger.Disable = disableLog }(logger.Disable)
|
||||
logger.Disable = true
|
||||
|
||||
for i, test := range generateKeyTests {
|
||||
i, test := i, test
|
||||
func() {
|
||||
defer recoverTest(i, test.ShouldPass, t)
|
||||
key := GenerateKey(test.ExtKey, test.Random)
|
||||
if [32]byte(key) == [32]byte{} {
|
||||
t.Errorf("Test %d: generated key is zero key", i) // check that we generate random and unique key
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var sealUnsealKeyTests = []struct {
|
||||
SealExtKey, SealIV [32]byte
|
||||
SealDomain, SealBucket, SealObject string
|
||||
|
||||
UnsealExtKey [32]byte
|
||||
UnsealDomain, UnsealBucket, UnsealObject string
|
||||
|
||||
ShouldPass bool
|
||||
}{
|
||||
{
|
||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
|
||||
UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object",
|
||||
ShouldPass: true,
|
||||
}, // 0
|
||||
{
|
||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
|
||||
UnsealExtKey: [32]byte{1}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object", // different ext-key
|
||||
ShouldPass: false,
|
||||
}, // 1
|
||||
{
|
||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-S3", SealBucket: "bucket", SealObject: "object",
|
||||
UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "object", // different domain
|
||||
ShouldPass: false,
|
||||
}, // 2
|
||||
{
|
||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
|
||||
UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "Bucket", UnsealObject: "object", // different bucket
|
||||
ShouldPass: false,
|
||||
}, // 3
|
||||
{
|
||||
SealExtKey: [32]byte{}, SealIV: [32]byte{}, SealDomain: "SSE-C", SealBucket: "bucket", SealObject: "object",
|
||||
UnsealExtKey: [32]byte{}, UnsealDomain: "SSE-C", UnsealBucket: "bucket", UnsealObject: "Object", // different object
|
||||
ShouldPass: false,
|
||||
}, // 4
|
||||
}
|
||||
|
||||
func TestSealUnsealKey(t *testing.T) {
|
||||
for i, test := range sealUnsealKeyTests {
|
||||
key := GenerateKey(test.SealExtKey, rand.Reader)
|
||||
sealedKey := key.Seal(test.SealExtKey, test.SealIV, test.SealDomain, test.SealBucket, test.SealObject)
|
||||
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)
|
||||
} else if err != nil && test.ShouldPass {
|
||||
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 {
|
||||
PartID uint32
|
||||
PartKey string
|
||||
}{
|
||||
{PartID: 0, PartKey: "aa7855e13839dd767cd5da7c1ff5036540c9264b7a803029315e55375287b4af"},
|
||||
{PartID: 1, PartKey: "a3e7181c6eed030fd52f79537c56c4d07da92e56d374ff1dd2043350785b37d8"},
|
||||
{PartID: 10000, PartKey: "f86e65c396ed52d204ee44bd1a0bbd86eb8b01b7354e67a3b3ae0e34dd5bd115"},
|
||||
}
|
||||
|
||||
func TestDerivePartKey(t *testing.T) {
|
||||
var key ObjectKey
|
||||
for i, test := range derivePartKeyTest {
|
||||
expectedPartKey, err := hex.DecodeString(test.PartKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d failed to decode expected part-key: %v", i, err)
|
||||
}
|
||||
partKey := key.DerivePartKey(test.PartID)
|
||||
if !bytes.Equal(partKey[:], expectedPartKey[:]) {
|
||||
t.Errorf("Test %d derives wrong part-key: got '%s' want: '%s'", i, hex.EncodeToString(partKey[:]), test.PartKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
"github.com/minio/sio"
|
||||
)
|
||||
|
||||
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 = "X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key"
|
||||
|
||||
// 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.
|
||||
S3KMSKeyID = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id"
|
||||
|
||||
// S3KMSSealedKey is the metadata key referencing the encrypted key generated
|
||||
// by KMS. It is only used for SSE-S3 + KMS.
|
||||
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"
|
||||
)
|
||||
|
||||
// EncryptSinglePart encrypts an io.Reader which must be the
|
||||
// the body of a single-part PUT request.
|
||||
func EncryptSinglePart(r io.Reader, key ObjectKey) io.Reader {
|
||||
r, err := sio.EncryptReader(r, sio.Config{MinVersion: sio.Version20, Key: key[:]})
|
||||
if err != nil {
|
||||
logger.CriticalIf(context.Background(), errors.New("Unable to encrypt io.Reader using object key"))
|
||||
}
|
||||
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
|
||||
// uploaded with the single-part PUT API. The offset and length
|
||||
// specify the requested range.
|
||||
func DecryptSinglePart(w io.Writer, offset, length int64, key ObjectKey) io.WriteCloser {
|
||||
const PayloadSize = 1 << 16 // DARE 2.0
|
||||
w = ioutil.LimitedWriter(w, offset%PayloadSize, length)
|
||||
|
||||
decWriter, err := sio.DecryptWriter(w, sio.Config{Key: key[:]})
|
||||
if err != nil {
|
||||
logger.CriticalIf(context.Background(), errors.New("Unable to decrypt io.Writer using object key"))
|
||||
}
|
||||
return decWriter
|
||||
}
|
||||
@@ -18,13 +18,18 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/ellipses"
|
||||
)
|
||||
|
||||
// CacheConfig represents cache config settings
|
||||
type CacheConfig struct {
|
||||
Drives []string `json:"drives"`
|
||||
Expiry int `json:"expiry"`
|
||||
MaxUse int `json:"maxuse"`
|
||||
Exclude []string `json:"exclude"`
|
||||
}
|
||||
|
||||
@@ -40,6 +45,15 @@ func (cfg *CacheConfig) UnmarshalJSON(data []byte) (err error) {
|
||||
if err = json.Unmarshal(data, _cfg); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -51,12 +65,42 @@ func (cfg *CacheConfig) UnmarshalJSON(data []byte) (err error) {
|
||||
|
||||
// Parses given cacheDrivesEnv and returns a list of cache drives.
|
||||
func parseCacheDrives(drives []string) ([]string, error) {
|
||||
if len(drives) == 0 {
|
||||
return drives, nil
|
||||
}
|
||||
var endpoints []string
|
||||
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) {
|
||||
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.
|
||||
|
||||
@@ -41,12 +41,32 @@ func TestParseCacheDrives(t *testing.T) {
|
||||
expectedPatterns []string
|
||||
success bool
|
||||
}{"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 {
|
||||
testCases = append(testCases, struct {
|
||||
driveStr string
|
||||
expectedPatterns []string
|
||||
success bool
|
||||
}{"/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 {
|
||||
drives, err := parseCacheDrives(strings.Split(testCase.driveStr, cacheEnvDelimiter))
|
||||
|
||||
+14
-27
@@ -32,19 +32,14 @@ import (
|
||||
"github.com/djherbis/atime"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
|
||||
"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 (
|
||||
// disk cache needs to have cacheSizeMultiplier * object size space free for a cache entry to be created.
|
||||
cacheSizeMultiplier = 100
|
||||
cacheTrashDir = "trash"
|
||||
cacheMaxDiskUsagePct = 80 // in %
|
||||
cacheCleanupInterval = 10 // in minutes
|
||||
)
|
||||
|
||||
@@ -239,7 +234,7 @@ func (c cacheObjects) GetObject(ctx context.Context, bucket, object string, star
|
||||
pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
pipeWriter.Close() // Close writer explicitly signalling we wrote all data.
|
||||
pipeWriter.Close() // Close writer explicitly signaling we wrote all data.
|
||||
}()
|
||||
err = dcache.Put(ctx, bucket, object, hashReader, c.getMetadata(objInfo))
|
||||
if err != nil {
|
||||
@@ -292,40 +287,32 @@ func (c cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string)
|
||||
// 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.
|
||||
// disks - list of fsObjects
|
||||
func listDirCacheFactory(isLeaf isLeafFunc, treeWalkIgnoredErrs []error, disks []*cacheFSObjects) listDirFunc {
|
||||
listCacheDirs := func(bucket, prefixDir, prefixEntry string) (dirs []string, err error) {
|
||||
func listDirCacheFactory(isLeaf isLeafFunc, disks []*cacheFSObjects) listDirFunc {
|
||||
listCacheDirs := func(bucket, prefixDir, prefixEntry string) (dirs []string) {
|
||||
var entries []string
|
||||
for _, disk := range disks {
|
||||
// ignore disk-caches that might be missing/offline
|
||||
if disk == nil {
|
||||
continue
|
||||
}
|
||||
fs := disk.FSObjects
|
||||
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
||||
|
||||
// For any reason disk was deleted or goes offline, continue
|
||||
// and list from other disks if possible.
|
||||
fs := disk.FSObjects
|
||||
var err error
|
||||
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
||||
if err != nil {
|
||||
if IsErrIgnored(err, treeWalkIgnoredErrs...) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter entries that have the prefix prefixEntry.
|
||||
entries = filterMatchingPrefix(entries, prefixEntry)
|
||||
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 := func(bucket, prefixDir, prefixEntry string) (mergedEntries []string, delayIsLeaf bool, err error) {
|
||||
var cacheEntries []string
|
||||
cacheEntries, err = listCacheDirs(bucket, prefixDir, prefixEntry)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
listDir := func(bucket, prefixDir, prefixEntry string) (mergedEntries []string, delayIsLeaf bool) {
|
||||
cacheEntries := listCacheDirs(bucket, prefixDir, prefixEntry)
|
||||
for _, entry := range cacheEntries {
|
||||
// Find elements in entries which are not in mergedEntries
|
||||
idx := sort.SearchStrings(mergedEntries, entry)
|
||||
@@ -336,7 +323,7 @@ func listDirCacheFactory(isLeaf isLeafFunc, treeWalkIgnoredErrs []error, disks [
|
||||
mergedEntries = append(mergedEntries, entry)
|
||||
sort.Strings(mergedEntries)
|
||||
}
|
||||
return mergedEntries, false, nil
|
||||
return mergedEntries, false
|
||||
}
|
||||
return listDir
|
||||
}
|
||||
@@ -372,7 +359,7 @@ func (c cacheObjects) listCacheObjects(ctx context.Context, bucket, prefix, mark
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -839,7 +826,7 @@ func newCache(config CacheConfig) (*diskCache, error) {
|
||||
if err := checkAtimeSupport(dir); err != nil {
|
||||
return nil, errors.New("Atime support required for disk caching")
|
||||
}
|
||||
cache, err := newCacheFSObjects(dir, config.Expiry, cacheMaxDiskUsagePct)
|
||||
cache, err := newCacheFSObjects(dir, config.Expiry, config.MaxUse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+126
-6
@@ -28,10 +28,10 @@ import (
|
||||
)
|
||||
|
||||
// Initialize cache FS objects.
|
||||
func initCacheFSObjects(disk string, t *testing.T) (*cacheFSObjects, error) {
|
||||
func initCacheFSObjects(disk string, cacheMaxUse int, t *testing.T) (*cacheFSObjects, error) {
|
||||
newTestConfig(globalMinioDefaultRegion)
|
||||
var err error
|
||||
obj, err := newCacheFSObjects(disk, globalCacheExpiry, 100)
|
||||
obj, err := newCacheFSObjects(disk, globalCacheExpiry, cacheMaxUse)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -39,10 +39,10 @@ func initCacheFSObjects(disk string, t *testing.T) (*cacheFSObjects, error) {
|
||||
}
|
||||
|
||||
// inits diskCache struct for nDisks
|
||||
func initDiskCaches(drives []string, t *testing.T) (*diskCache, error) {
|
||||
func initDiskCaches(drives []string, cacheMaxUse int, t *testing.T) (*diskCache, error) {
|
||||
var cfs []*cacheFSObjects
|
||||
for _, d := range drives {
|
||||
obj, err := initCacheFSObjects(d, t)
|
||||
obj, err := initCacheFSObjects(d, cacheMaxUse, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,7 +59,46 @@ func TestGetCacheFS(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := initDiskCaches(fsDirs, t)
|
||||
d, err := initDiskCaches(fsDirs, 100, t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bucketName := "testbucket"
|
||||
objectName := "testobject"
|
||||
ctx := context.Background()
|
||||
// find cache drive where object would be hashed
|
||||
index := d.hashIndex(bucketName, objectName)
|
||||
// turn off drive by setting online status to false
|
||||
d.cfs[index].online = false
|
||||
cfs, err := d.getCacheFS(ctx, bucketName, objectName)
|
||||
if n == 1 && err == errDiskNotFound {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
i := -1
|
||||
for j, f := range d.cfs {
|
||||
if f == cfs {
|
||||
i = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if i != (index+1)%n {
|
||||
t.Fatalf("expected next cache location to be picked")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test whether a drive being offline causes
|
||||
// getCacheFS to fetch next online drive
|
||||
func TestGetCacheFSMaxUse(t *testing.T) {
|
||||
for n := 1; n < 10; n++ {
|
||||
fsDirs, err := getRandomDisks(n)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := initDiskCaches(fsDirs, globalCacheMaxUse, t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -141,7 +180,7 @@ func TestDiskCache(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := initDiskCaches(fsDirs, t)
|
||||
d, err := initDiskCaches(fsDirs, 100, t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -209,6 +248,87 @@ func TestDiskCache(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test diskCache with upper bound on max cache use.
|
||||
func TestDiskCacheMaxUse(t *testing.T) {
|
||||
fsDirs, err := getRandomDisks(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := initDiskCaches(fsDirs, globalCacheMaxUse, t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cache := d.cfs[0]
|
||||
ctx := context.Background()
|
||||
bucketName := "testbucket"
|
||||
objectName := "testobject"
|
||||
content := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
etag := "061208c10af71a30c6dcd6cf5d89f0fe"
|
||||
contentType := "application/zip"
|
||||
size := len(content)
|
||||
|
||||
httpMeta := make(map[string]string)
|
||||
httpMeta["etag"] = etag
|
||||
httpMeta["content-type"] = contentType
|
||||
|
||||
objInfo := ObjectInfo{}
|
||||
objInfo.Bucket = bucketName
|
||||
objInfo.Name = objectName
|
||||
objInfo.Size = int64(size)
|
||||
objInfo.ContentType = contentType
|
||||
objInfo.ETag = etag
|
||||
objInfo.UserDefined = httpMeta
|
||||
|
||||
byteReader := bytes.NewReader([]byte(content))
|
||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cache.diskAvailable(int64(size)) {
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta)
|
||||
if err != errDiskFull {
|
||||
t.Fatal("Cache max-use limit violated.")
|
||||
}
|
||||
} else {
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, httpMeta)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cachedObjInfo, err := cache.GetObjectInfo(ctx, bucketName, objectName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cache.Exists(ctx, bucketName, objectName) {
|
||||
t.Fatal("Expected object to exist on cache")
|
||||
}
|
||||
if cachedObjInfo.ETag != objInfo.ETag {
|
||||
t.Fatal("Expected ETag to match")
|
||||
}
|
||||
if cachedObjInfo.Size != objInfo.Size {
|
||||
t.Fatal("Size mismatch")
|
||||
}
|
||||
if cachedObjInfo.ContentType != objInfo.ContentType {
|
||||
t.Fatal("Cached content-type does not match")
|
||||
}
|
||||
writer := bytes.NewBuffer(nil)
|
||||
err = cache.Get(ctx, bucketName, objectName, 0, int64(size), writer, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ccontent := writer.Bytes(); !bytes.Equal([]byte(content), ccontent) {
|
||||
t.Errorf("wrong cached file content")
|
||||
}
|
||||
err = cache.Delete(ctx, bucketName, objectName)
|
||||
if err != nil {
|
||||
t.Errorf("object missing from cache")
|
||||
}
|
||||
online := cache.IsOnline()
|
||||
if !online {
|
||||
t.Errorf("expected cache drive to be online")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCacheExcludeDirective(t *testing.T) {
|
||||
testCases := []struct {
|
||||
cacheControlOpt string
|
||||
|
||||
+186
-86
@@ -28,6 +28,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -144,9 +145,20 @@ const (
|
||||
ServerSideEncryptionSealedKey = ReservedMetadataPrefix + "Server-Side-Encryption-Sealed-Key"
|
||||
)
|
||||
|
||||
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||
// hash function.
|
||||
const SSESealAlgorithmDareSha256 = "DARE-SHA256"
|
||||
const (
|
||||
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||
// hash function. The key derivation of DARE-SHA256 is not optimal and does not include the object path.
|
||||
// It is considered legacy and should not be used anymore.
|
||||
SSESealAlgorithmDareSha256 = "DARE-SHA256"
|
||||
|
||||
// SSESealAlgorithmDareV2HmacSha256 specifies DAREv2 as authenticated en/decryption scheme and SHA256 as cryptographic
|
||||
// hash function for the HMAC PRF.
|
||||
SSESealAlgorithmDareV2HmacSha256 = "DAREv2-HMAC-SHA256"
|
||||
|
||||
// SSEDomain specifies the domain for the derived key - in this case the
|
||||
// key should be used for SSE-C.
|
||||
SSEDomain = "SSE-C"
|
||||
)
|
||||
|
||||
// hasSSECustomerHeader returns true if the given HTTP header
|
||||
// contains server-side-encryption with customer provided key fields.
|
||||
@@ -250,10 +262,11 @@ func ParseSSECustomerHeader(header http.Header) (key []byte, err error) {
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
if metadata[ServerSideEncryptionSealAlgorithm] != SSESealAlgorithmDareSha256 { // currently DARE-SHA256 is the only option
|
||||
algorithm := metadata[ServerSideEncryptionSealAlgorithm]
|
||||
if algorithm != SSESealAlgorithmDareSha256 && algorithm != SSESealAlgorithmDareV2HmacSha256 {
|
||||
return errObjectTampered
|
||||
}
|
||||
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV])
|
||||
@@ -265,14 +278,33 @@ func rotateKey(oldKey []byte, newKey []byte, metadata map[string]string) error {
|
||||
return errObjectTampered
|
||||
}
|
||||
|
||||
sha := sha256.New() // derive key encryption key
|
||||
sha.Write(oldKey)
|
||||
sha.Write(iv)
|
||||
keyEncryptionKey := sha.Sum(nil)
|
||||
var (
|
||||
minDAREVersion byte
|
||||
keyEncryptionKey [32]byte
|
||||
)
|
||||
switch algorithm {
|
||||
default:
|
||||
return errObjectTampered
|
||||
case SSESealAlgorithmDareSha256: // legacy key-encryption-key derivation
|
||||
minDAREVersion = sio.Version10
|
||||
sha := sha256.New()
|
||||
sha.Write(oldKey)
|
||||
sha.Write(iv)
|
||||
sha.Sum(keyEncryptionKey[:0])
|
||||
case SSESealAlgorithmDareV2HmacSha256: // key-encryption-key derivation - See: crypto/doc.go
|
||||
minDAREVersion = sio.Version20
|
||||
mac := hmac.New(sha256.New, oldKey)
|
||||
mac.Write(iv)
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(keyEncryptionKey[:0])
|
||||
}
|
||||
|
||||
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
|
||||
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
|
||||
Key: keyEncryptionKey,
|
||||
MinVersion: minDAREVersion,
|
||||
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 {
|
||||
@@ -280,46 +312,34 @@ func rotateKey(oldKey []byte, newKey []byte, metadata map[string]string) error {
|
||||
}
|
||||
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
|
||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && algorithm != SSESealAlgorithmDareSha256 {
|
||||
return nil // we don't need to rotate keys if newKey == oldKey but we may have to upgrade KDF algorithm
|
||||
}
|
||||
|
||||
nonce := make([]byte, 32) // generate random values for key derivation
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
niv := sha256.Sum256(nonce[:]) // derive key encryption key
|
||||
sha = sha256.New()
|
||||
sha.Write(newKey)
|
||||
sha.Write(niv[:])
|
||||
keyEncryptionKey = sha.Sum(nil)
|
||||
mac := hmac.New(sha256.New, newKey) // key-encryption-key derivation - See: crypto/doc.go
|
||||
mac.Write(iv)
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(keyEncryptionKey[:0])
|
||||
|
||||
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,
|
||||
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[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(iv[:])
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareV2HmacSha256
|
||||
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) ([]byte, error) {
|
||||
delete(metadata, SSECustomerKey) // make sure we do not save the key by accident
|
||||
|
||||
// security notice:
|
||||
// - If the first 32 bytes of the random value are ever repeated under the same client-provided
|
||||
// key the encrypted object will not be tamper-proof. [ P(coll) ~= 1 / 2^(256 / 2)]
|
||||
// - If the last 32 bytes of the random value are ever repeated under the same client-provided
|
||||
// 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
|
||||
// be repeated in addition to reveal the object encryption key.
|
||||
// [ P(coll) ~= 1 / 2^((256 + 64) / 2) ]
|
||||
// See crypto/doc.go for detailed description
|
||||
nonce := make([]byte, 32+SSEIVSize) // generate random values for key derivation
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
@@ -329,11 +349,13 @@ func newEncryptMetadata(key []byte, metadata map[string]string) ([]byte, error)
|
||||
sha.Write(nonce[:32])
|
||||
objectEncryptionKey := sha.Sum(nil)
|
||||
|
||||
iv := sha256.Sum256(nonce[32:]) // derive key encryption key
|
||||
sha = sha256.New()
|
||||
sha.Write(key)
|
||||
sha.Write(iv[:])
|
||||
keyEncryptionKey := sha.Sum(nil)
|
||||
iv := sha256.Sum256(nonce[32:]) // key-encryption-key derivation - See: crypto/doc.go
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(iv[:])
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
keyEncryptionKey := mac.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{
|
||||
@@ -344,14 +366,14 @@ func newEncryptMetadata(key []byte, metadata map[string]string) ([]byte, error)
|
||||
}
|
||||
|
||||
metadata[ServerSideEncryptionIV] = base64.StdEncoding.EncodeToString(iv[:])
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareSha256
|
||||
metadata[ServerSideEncryptionSealAlgorithm] = SSESealAlgorithmDareV2HmacSha256
|
||||
metadata[ServerSideEncryptionSealedKey] = base64.StdEncoding.EncodeToString(sealedKey.Bytes())
|
||||
|
||||
return objectEncryptionKey, nil
|
||||
}
|
||||
|
||||
func newEncryptReader(content io.Reader, key []byte, metadata map[string]string) (io.Reader, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(key, metadata)
|
||||
func newEncryptReader(content io.Reader, key []byte, bucket, object string, metadata map[string]string) (io.Reader, error) {
|
||||
objectEncryptionKey, err := newEncryptMetadata(key, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -367,29 +389,26 @@ func newEncryptReader(content io.Reader, key []byte, metadata map[string]string)
|
||||
// EncryptRequest takes the client provided content and encrypts the data
|
||||
// with the client provided key. It also marks the object as client-side-encrypted
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
return newEncryptReader(content, key, metadata)
|
||||
return newEncryptReader(content, key, bucket, object, metadata)
|
||||
}
|
||||
|
||||
// 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.
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(metadata, SSECopyCustomerKey) // 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) {
|
||||
if metadata[ServerSideEncryptionSealAlgorithm] != SSESealAlgorithmDareSha256 { // currently DARE-SHA256 is the only option
|
||||
return nil, errObjectTampered
|
||||
}
|
||||
func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
iv, err := base64.StdEncoding.DecodeString(metadata[ServerSideEncryptionIV])
|
||||
if err != nil || len(iv) != SSEIVSize {
|
||||
return nil, errObjectTampered
|
||||
@@ -399,14 +418,33 @@ func decryptObjectInfo(key []byte, metadata map[string]string) ([]byte, error) {
|
||||
return nil, errObjectTampered
|
||||
}
|
||||
|
||||
sha := sha256.New() // derive key encryption key
|
||||
sha.Write(key)
|
||||
sha.Write(iv)
|
||||
keyEncryptionKey := sha.Sum(nil)
|
||||
var (
|
||||
minDAREVersion byte
|
||||
keyEncryptionKey [32]byte
|
||||
)
|
||||
switch algorithm := metadata[ServerSideEncryptionSealAlgorithm]; algorithm {
|
||||
default:
|
||||
return nil, errObjectTampered
|
||||
case SSESealAlgorithmDareSha256: // legacy key-encryption-key derivation
|
||||
minDAREVersion = sio.Version10
|
||||
sha := sha256.New()
|
||||
sha.Write(key)
|
||||
sha.Write(iv)
|
||||
sha.Sum(keyEncryptionKey[:0])
|
||||
case SSESealAlgorithmDareV2HmacSha256: // key-encryption-key derivation - See: crypto/doc.go
|
||||
minDAREVersion = sio.Version20
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write(iv)
|
||||
mac.Write([]byte(SSEDomain))
|
||||
mac.Write([]byte(SSESealAlgorithmDareV2HmacSha256))
|
||||
mac.Write([]byte(path.Join(bucket, object)))
|
||||
mac.Sum(keyEncryptionKey[:0])
|
||||
}
|
||||
|
||||
objectEncryptionKey := bytes.NewBuffer(nil) // decrypt object encryption key
|
||||
n, err := sio.Decrypt(objectEncryptionKey, bytes.NewReader(sealedKey), sio.Config{
|
||||
Key: keyEncryptionKey,
|
||||
MinVersion: minDAREVersion,
|
||||
Key: keyEncryptionKey[:],
|
||||
})
|
||||
if n != 32 || err != nil {
|
||||
// Either the provided key does not match or the object was tampered.
|
||||
@@ -416,11 +454,10 @@ func decryptObjectInfo(key []byte, metadata map[string]string) ([]byte, error) {
|
||||
return objectEncryptionKey.Bytes(), nil
|
||||
}
|
||||
|
||||
func newDecryptWriter(client io.Writer, key []byte, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
objectEncryptionKey, err := decryptObjectInfo(key, metadata)
|
||||
func newDecryptWriter(client io.Writer, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
objectEncryptionKey, err := decryptObjectInfo(key, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
}
|
||||
return newDecryptWriterWithObjectKey(client, objectEncryptionKey, seqNumber, metadata)
|
||||
}
|
||||
@@ -443,19 +480,19 @@ func newDecryptWriterWithObjectKey(client io.Writer, objectEncryptionKey []byte,
|
||||
|
||||
// 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.
|
||||
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) {
|
||||
key, err := ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(metadata, SSECustomerKey) // 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
|
||||
// the client-side-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) {
|
||||
return DecryptRequestWithSequenceNumber(client, r, 0, metadata)
|
||||
func DecryptRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
|
||||
return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
|
||||
}
|
||||
|
||||
// DecryptBlocksWriter - decrypts multipart parts, while implementing a io.Writer compatible interface.
|
||||
@@ -469,9 +506,10 @@ type DecryptBlocksWriter struct {
|
||||
// Current part index
|
||||
partIndex int
|
||||
// Parts information
|
||||
parts []objectPartInfo
|
||||
req *http.Request
|
||||
metadata map[string]string
|
||||
parts []objectPartInfo
|
||||
req *http.Request
|
||||
bucket, object string
|
||||
metadata map[string]string
|
||||
|
||||
partEncRelOffset int64
|
||||
|
||||
@@ -499,7 +537,7 @@ func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
objectEncryptionKey, err := decryptObjectInfo(key, m)
|
||||
objectEncryptionKey, err := decryptObjectInfo(key, w.bucket, w.object, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -594,29 +632,26 @@ func (w *DecryptBlocksWriter) Close() error {
|
||||
// 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
|
||||
// all parts starting from part-1.
|
||||
func DecryptAllBlocksCopyRequest(client io.Writer, r *http.Request, objInfo ObjectInfo) (io.WriteCloser, int64, error) {
|
||||
w, _, size, err := DecryptBlocksRequest(client, r, 0, objInfo.Size, objInfo, true)
|
||||
func DecryptAllBlocksCopyRequest(client io.Writer, r *http.Request, bucket, object string, objInfo ObjectInfo) (io.WriteCloser, int64, error) {
|
||||
w, _, size, err := DecryptBlocksRequest(client, r, bucket, object, 0, objInfo.Size, objInfo, true)
|
||||
return w, size, err
|
||||
}
|
||||
|
||||
// DecryptBlocksRequest - setup a struct which can decrypt many concatenated encrypted data
|
||||
// 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) {
|
||||
seqNumber, encStartOffset, encLength := getEncryptedStartOffset(startOffset, length)
|
||||
|
||||
// Encryption length cannot be bigger than the file size, if it is
|
||||
// which is allowed in AWS S3, we simply default to EncryptedSize().
|
||||
if encLength+encStartOffset > objInfo.EncryptedSize() {
|
||||
encLength = objInfo.EncryptedSize() - encStartOffset
|
||||
}
|
||||
func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object string, startOffset, length int64, objInfo ObjectInfo, copySource bool) (io.WriteCloser, int64, int64, error) {
|
||||
var seqNumber uint32
|
||||
var encStartOffset, encLength int64
|
||||
|
||||
if len(objInfo.Parts) == 0 || !objInfo.IsEncryptedMultipart() {
|
||||
seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
|
||||
|
||||
var writer io.WriteCloser
|
||||
var err error
|
||||
if copySource {
|
||||
writer, err = DecryptCopyRequest(client, r, objInfo.UserDefined)
|
||||
writer, err = DecryptCopyRequest(client, r, bucket, object, objInfo.UserDefined)
|
||||
} else {
|
||||
writer, err = DecryptRequestWithSequenceNumber(client, r, seqNumber, objInfo.UserDefined)
|
||||
writer, err = DecryptRequestWithSequenceNumber(client, r, bucket, object, seqNumber, objInfo.UserDefined)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
@@ -624,6 +659,7 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
|
||||
return writer, encStartOffset, encLength, nil
|
||||
}
|
||||
|
||||
seqNumber, encStartOffset, encLength = getEncryptedMultipartsOffsetLength(startOffset, length, objInfo)
|
||||
var partStartIndex int
|
||||
var partStartOffset = startOffset
|
||||
// Skip parts until final offset maps to a particular part offset.
|
||||
@@ -656,6 +692,8 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
|
||||
parts: objInfo.Parts,
|
||||
partIndex: partStartIndex,
|
||||
req: r,
|
||||
bucket: bucket,
|
||||
object: object,
|
||||
customerKeyHeader: r.Header.Get(SSECustomerKey),
|
||||
copySource: copySource,
|
||||
}
|
||||
@@ -676,15 +714,62 @@ func DecryptBlocksRequest(client io.Writer, r *http.Request, startOffset, length
|
||||
w.customerKeyHeader = r.Header.Get(SSECopyCustomerKey)
|
||||
}
|
||||
|
||||
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 w, encStartOffset, encLength, nil
|
||||
}
|
||||
|
||||
// getEncryptedStartOffset - fetch sequence number, encrypted start offset and encrypted length.
|
||||
func getEncryptedStartOffset(offset, length int64) (seqNumber uint32, encOffset int64, encLength int64) {
|
||||
// getEncryptedMultipartsOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
|
||||
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)
|
||||
|
||||
seqNumber = uint32(offset / sseDAREPackageBlockSize)
|
||||
@@ -702,6 +787,9 @@ func getEncryptedStartOffset(offset, length int64) (seqNumber uint32, encOffset
|
||||
encLength += onePkgSize
|
||||
}
|
||||
|
||||
if encLength+encOffset > objInfo.EncryptedSize() {
|
||||
encLength = objInfo.EncryptedSize() - encOffset
|
||||
}
|
||||
return seqNumber, encOffset, encLength
|
||||
}
|
||||
|
||||
@@ -746,11 +834,23 @@ func (o *ObjectInfo) DecryptedSize() (int64, error) {
|
||||
if !o.IsEncrypted() {
|
||||
return 0, errors.New("Cannot compute decrypted size of an unencrypted object")
|
||||
}
|
||||
size, err := sio.DecryptedSize(uint64(o.Size))
|
||||
if err != nil {
|
||||
err = errObjectTampered // assign correct error type
|
||||
if len(o.Parts) == 0 || !o.IsEncryptedMultipart() {
|
||||
size, err := sio.DecryptedSize(uint64(o.Size))
|
||||
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.
|
||||
|
||||
@@ -308,7 +308,7 @@ func TestEncryptRequest(t *testing.T) {
|
||||
for k, v := range test.header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
_, err := EncryptRequest(content, req, test.metadata)
|
||||
_, err := EncryptRequest(content, req, "bucket", "object", test.metadata)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||
}
|
||||
@@ -328,11 +328,14 @@ func TestEncryptRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
var decryptRequestTests = []struct {
|
||||
header map[string]string
|
||||
metadata map[string]string
|
||||
shouldFail bool
|
||||
bucket, object string
|
||||
header map[string]string
|
||||
metadata map[string]string
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
@@ -346,6 +349,23 @@ var decryptRequestTests = []struct {
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
SSECustomerKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
||||
ServerSideEncryptionIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||
ServerSideEncryptionSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||
},
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
@@ -359,6 +379,8 @@ var decryptRequestTests = []struct {
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
@@ -372,6 +394,8 @@ var decryptRequestTests = []struct {
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
@@ -384,21 +408,39 @@ var decryptRequestTests = []struct {
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object-2",
|
||||
header: map[string]string{
|
||||
SSECustomerAlgorithm: "AES256",
|
||||
SSECustomerKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
SSECustomerKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
ServerSideEncryptionSealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
||||
ServerSideEncryptionIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||
ServerSideEncryptionSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestDecryptRequest(t *testing.T) {
|
||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
||||
globalIsSSL = true
|
||||
for i, test := range decryptRequestTests {
|
||||
for i, test := range decryptRequestTests[1:] {
|
||||
client := bytes.NewBuffer(nil)
|
||||
req := &http.Request{Header: http.Header{}}
|
||||
for k, v := range test.header {
|
||||
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 {
|
||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||
}
|
||||
if err == nil && test.shouldFail {
|
||||
t.Fatalf("Test %d: should fail but passed", i)
|
||||
}
|
||||
if key, ok := test.metadata[SSECustomerKey]; ok {
|
||||
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -442,6 +443,8 @@ func CreateEndpoints(serverAddr string, args ...[]string) (string, EndpointList,
|
||||
return serverAddr, endpoints, setupType, err
|
||||
}
|
||||
|
||||
updateDomainIPs(uniqueArgs)
|
||||
|
||||
setupType = DistXLSetupType
|
||||
return serverAddr, endpoints, setupType, nil
|
||||
}
|
||||
@@ -493,3 +496,22 @@ func GetRemotePeers(endpoints EndpointList) []string {
|
||||
|
||||
return peerSet.ToSlice()
|
||||
}
|
||||
|
||||
// In federated and distributed setup, update IP addresses of the hosts passed in command line
|
||||
// if MINIO_PUBLIC_IPS are not set manually
|
||||
func updateDomainIPs(endPoints set.StringSet) {
|
||||
_, dok := os.LookupEnv("MINIO_DOMAIN")
|
||||
_, eok := os.LookupEnv("MINIO_ETCD_ENDPOINTS")
|
||||
_, iok := os.LookupEnv("MINIO_PUBLIC_IPS")
|
||||
if dok && eok && !iok {
|
||||
globalDomainIPs = set.NewStringSet()
|
||||
for e := range endPoints {
|
||||
host, _, _ := net.SplitHostPort(e)
|
||||
ipList, _ := getHostIP4(host)
|
||||
remoteIPList := ipList.FuncMatch(func(ip string, matchString string) bool {
|
||||
return !strings.HasPrefix(ip, "127.")
|
||||
}, "")
|
||||
globalDomainIPs.Add(remoteIPList.ToSlice()[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+114
-12
@@ -24,6 +24,7 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
)
|
||||
|
||||
@@ -55,24 +56,22 @@ type formatFSVersionDetect struct {
|
||||
} `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
|
||||
func newFormatFSV1() (format *formatFSV1) {
|
||||
f := &formatFSV1{}
|
||||
f.Version = formatMetaVersionV1
|
||||
f.Format = formatBackendFS
|
||||
f.ID = mustGetUUID()
|
||||
f.FS.Version = formatFSVersionV1
|
||||
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.
|
||||
// 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) {
|
||||
@@ -115,7 +114,16 @@ func formatFSMigrateV1ToV2(ctx context.Context, wlk *lock.LockedFile, fsPath str
|
||||
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.
|
||||
@@ -179,6 +187,12 @@ func createFormatFS(ctx context.Context, fsFormatPath string) error {
|
||||
// migrate the backend when we are actively working on the backend.
|
||||
func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, err error) {
|
||||
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 write on format.json should be done with write-lock.
|
||||
for {
|
||||
@@ -235,7 +249,8 @@ func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, er
|
||||
rlk.Close()
|
||||
// Hold write lock during migration so that we do not disturb any
|
||||
// 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 {
|
||||
// Lock already present, sleep and attempt again.
|
||||
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
|
||||
continue
|
||||
}
|
||||
|
||||
var id string
|
||||
if id, err = formatFSGetDeploymentID(rlk); err != nil {
|
||||
rlk.Close()
|
||||
return nil, err
|
||||
}
|
||||
logger.SetDeploymentID(id)
|
||||
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"`
|
||||
// Format indicates the backend format type, supports two values 'xl' and 'fs'.
|
||||
Format string `json:"format"`
|
||||
// ID is the identifier for the minio deployment
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
+115
-6
@@ -22,11 +22,13 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"encoding/hex"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
sha256 "github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
@@ -86,10 +88,11 @@ type formatXLV1 struct {
|
||||
// Represents the V2 backend disk structure version
|
||||
// under `.minio.sys` and actual data namespace.
|
||||
// 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 {
|
||||
Version string `json:"version"`
|
||||
Format string `json:"format"`
|
||||
XL struct {
|
||||
formatMetaV1
|
||||
XL struct {
|
||||
Version string `json:"version"` // Version of 'xl' format.
|
||||
This string `json:"this"` // This field carries assigned disk uuid.
|
||||
// Sets field carries the input disk order generated the first
|
||||
@@ -107,9 +110,8 @@ type formatXLV2 struct {
|
||||
// In .minio.sys/multipart we have:
|
||||
// sha256(bucket/object)/uploadID/[xl.json, part.1, part.2 ....]
|
||||
type formatXLV3 struct {
|
||||
Version string `json:"version"`
|
||||
Format string `json:"format"`
|
||||
XL struct {
|
||||
formatMetaV1
|
||||
XL struct {
|
||||
Version string `json:"version"` // Version of 'xl' format.
|
||||
This string `json:"this"` // This field carries assigned disk uuid.
|
||||
// Sets field carries the input disk order generated the first
|
||||
@@ -127,6 +129,7 @@ func newFormatXLV3(numSets int, setLen int) *formatXLV3 {
|
||||
format := &formatXLV3{}
|
||||
format.Version = formatMetaVersionV1
|
||||
format.Format = formatBackendXL
|
||||
format.ID = mustGetUUID()
|
||||
format.XL.Version = formatXLVersionV3
|
||||
format.XL.DistributionAlgo = formatXLVersionV2DistributionAlgo
|
||||
format.XL.Sets = make([][]string, numSets)
|
||||
@@ -443,6 +446,112 @@ func checkFormatXLValues(formats []*formatXLV3) error {
|
||||
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`.
|
||||
func getFormatXLInQuorum(formats []*formatXLV3) (*formatXLV3, error) {
|
||||
formatHashes := make([]string, len(formats))
|
||||
|
||||
+67
-6
@@ -345,8 +345,10 @@ func TestCheckFormatXLValue(t *testing.T) {
|
||||
// Invalid XL format version "2".
|
||||
{
|
||||
&formatXLV3{
|
||||
Version: "2",
|
||||
Format: "XL",
|
||||
formatMetaV1: formatMetaV1{
|
||||
Version: "2",
|
||||
Format: "XL",
|
||||
},
|
||||
XL: struct {
|
||||
Version string `json:"version"`
|
||||
This string `json:"this"`
|
||||
@@ -361,8 +363,10 @@ func TestCheckFormatXLValue(t *testing.T) {
|
||||
// Invalid XL format "Unknown".
|
||||
{
|
||||
&formatXLV3{
|
||||
Version: "1",
|
||||
Format: "Unknown",
|
||||
formatMetaV1: formatMetaV1{
|
||||
Version: "1",
|
||||
Format: "Unknown",
|
||||
},
|
||||
XL: struct {
|
||||
Version string `json:"version"`
|
||||
This string `json:"this"`
|
||||
@@ -377,8 +381,10 @@ func TestCheckFormatXLValue(t *testing.T) {
|
||||
// Invalid XL format version "0".
|
||||
{
|
||||
&formatXLV3{
|
||||
Version: "1",
|
||||
Format: "XL",
|
||||
formatMetaV1: formatMetaV1{
|
||||
Version: "1",
|
||||
Format: "XL",
|
||||
},
|
||||
XL: struct {
|
||||
Version string `json:"version"`
|
||||
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.
|
||||
func TestNewFormatSets(t *testing.T) {
|
||||
setCount := 2
|
||||
|
||||
@@ -178,7 +178,6 @@ func fsStatVolume(ctx context.Context, volume string) (os.FileInfo, error) {
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
logger.LogIf(ctx, errVolumeAccessDenied)
|
||||
return nil, errVolumeAccessDenied
|
||||
}
|
||||
|
||||
@@ -200,7 +199,7 @@ func osErrToFSFileErr(err error) error {
|
||||
return errFileAccessDenied
|
||||
}
|
||||
if isSysErrNotDir(err) {
|
||||
return errFileAccessDenied
|
||||
return errFileNotFound
|
||||
}
|
||||
if isSysErrPathNotFound(err) {
|
||||
return errFileNotFound
|
||||
@@ -219,8 +218,7 @@ func fsStatDir(ctx context.Context, statDir string) (os.FileInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
logger.LogIf(ctx, errFileAccessDenied)
|
||||
return nil, errFileAccessDenied
|
||||
return nil, errFileNotFound
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
@@ -245,8 +243,7 @@ func fsStatFile(ctx context.Context, statFile string) (os.FileInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
logger.LogIf(ctx, errFileAccessDenied)
|
||||
return nil, errFileAccessDenied
|
||||
return nil, errFileNotFound
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ func TestFSStats(t *testing.T) {
|
||||
srcFSPath: path,
|
||||
srcVol: "success-vol",
|
||||
srcPath: "path",
|
||||
expectedErr: errFileAccessDenied,
|
||||
expectedErr: errFileNotFound,
|
||||
},
|
||||
// Test case - 6.
|
||||
// Test case with src path segment > 255.
|
||||
|
||||
+2
-16
@@ -264,7 +264,7 @@ func (fs *FSObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, d
|
||||
}
|
||||
return
|
||||
}
|
||||
// Close writer explicitly signalling we wrote all data.
|
||||
// Close writer explicitly signaling we wrote all data.
|
||||
if gerr := srcInfo.Writer.Close(); gerr != nil {
|
||||
logger.LogIf(ctx, gerr)
|
||||
return
|
||||
@@ -543,25 +543,11 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
|
||||
// All parts except the last part has to be atleast 5MB.
|
||||
if !isMinAllowedPartSize(fi.Size()) {
|
||||
err = PartTooSmall{
|
||||
return oi, PartTooSmall{
|
||||
PartNumber: part.PartNumber,
|
||||
PartSize: fi.Size(),
|
||||
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{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+72
-49
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/mimedb"
|
||||
"github.com/minio/minio/pkg/mountinfo"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
@@ -44,8 +45,6 @@ var defaultEtag = "00000000000000000000000000000000-1"
|
||||
type FSObjects struct {
|
||||
// Disk usage metrics
|
||||
totalUsed uint64 // ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||
// Disk usage running routine
|
||||
usageRunning int32 // ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||
|
||||
// Path to be exported over S3 API.
|
||||
fsPath string
|
||||
@@ -64,6 +63,8 @@ type FSObjects struct {
|
||||
// ListObjects pool management.
|
||||
listPool *treeWalkPool
|
||||
|
||||
diskMount bool
|
||||
|
||||
appendFileMap map[string]*fsAppendFile
|
||||
appendFileMapMu sync.Mutex
|
||||
|
||||
@@ -109,6 +110,9 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
|
||||
var err error
|
||||
if fsPath, err = getValidPath(fsPath); err != nil {
|
||||
if err == errMinDiskSize {
|
||||
return nil, err
|
||||
}
|
||||
return nil, uiErrUnableToWriteInBackend(err)
|
||||
}
|
||||
|
||||
@@ -138,6 +142,7 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
nsMutex: newNSLock(false),
|
||||
listPool: newTreeWalkPool(globalLookupTimeout),
|
||||
appendFileMap: make(map[string]*fsAppendFile),
|
||||
diskMount: mountinfo.IsLikelyMountPoint(fsPath),
|
||||
}
|
||||
|
||||
// Once the filesystem has initialized hold the read lock for
|
||||
@@ -146,17 +151,10 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
// or cause changes on backend format.
|
||||
fs.fsFormatRlk = rlk
|
||||
|
||||
// Initialize notification system.
|
||||
if err = globalNotificationSys.Init(fs); err != nil {
|
||||
return nil, uiErrUnableToReadFromBackend(err).Msg("Unable to initialize notification system")
|
||||
if !fs.diskMount {
|
||||
go fs.diskUsage(globalServiceDoneCh)
|
||||
}
|
||||
|
||||
// Initialize policy system.
|
||||
if err = globalPolicySys.Init(fs); err != nil {
|
||||
return nil, uiErrUnableToReadFromBackend(err).Msg("Unable to initialize policy system")
|
||||
}
|
||||
|
||||
go fs.diskUsage(globalServiceDoneCh)
|
||||
go fs.cleanupStaleMultipartUploads(ctx, globalMultipartCleanupInterval, globalMultipartExpiry, globalServiceDoneCh)
|
||||
|
||||
// Return successfully initialized object layer.
|
||||
@@ -173,32 +171,40 @@ func (fs *FSObjects) Shutdown(ctx context.Context) error {
|
||||
|
||||
// diskUsage returns du information for the posix path, in a continuous routine.
|
||||
func (fs *FSObjects) diskUsage(doneCh chan struct{}) {
|
||||
ticker := time.NewTicker(globalUsageCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
usageFn := func(ctx context.Context, entry string) error {
|
||||
var fi os.FileInfo
|
||||
var err error
|
||||
if hasSuffix(entry, slashSeparator) {
|
||||
fi, err = fsStatDir(ctx, entry)
|
||||
} else {
|
||||
fi, err = fsStatFile(ctx, entry)
|
||||
if globalHTTPServer != nil {
|
||||
// Wait at max 1 minute for an inprogress request
|
||||
// before proceeding to count the usage.
|
||||
waitCount := 60
|
||||
// Any requests in progress, delay the usage.
|
||||
for globalHTTPServer.GetRequestCount() > 0 && waitCount > 0 {
|
||||
waitCount--
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
select {
|
||||
case <-doneCh:
|
||||
return errWalkAbort
|
||||
default:
|
||||
var fi os.FileInfo
|
||||
var err error
|
||||
if hasSuffix(entry, slashSeparator) {
|
||||
fi, err = fsStatDir(ctx, entry)
|
||||
} else {
|
||||
fi, err = fsStatFile(ctx, entry)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
atomic.AddUint64(&fs.totalUsed, uint64(fi.Size()))
|
||||
}
|
||||
atomic.AddUint64(&fs.totalUsed, uint64(fi.Size()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if disk usage routine is running, if yes then return.
|
||||
if atomic.LoadInt32(&fs.usageRunning) == 1 {
|
||||
return
|
||||
}
|
||||
atomic.StoreInt32(&fs.usageRunning, 1)
|
||||
defer atomic.StoreInt32(&fs.usageRunning, 0)
|
||||
|
||||
if err := getDiskUsage(context.Background(), fs.fsPath, usageFn); err != nil {
|
||||
// Return this routine upon errWalkAbort, continue for any other error on purpose
|
||||
// so that we can start the routine freshly in another 12 hours.
|
||||
if err := getDiskUsage(context.Background(), fs.fsPath, usageFn); err == errWalkAbort {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -206,15 +212,20 @@ func (fs *FSObjects) diskUsage(doneCh chan struct{}) {
|
||||
select {
|
||||
case <-doneCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Check if disk usage routine is running, if yes let it finish.
|
||||
if atomic.LoadInt32(&fs.usageRunning) == 1 {
|
||||
continue
|
||||
}
|
||||
atomic.StoreInt32(&fs.usageRunning, 1)
|
||||
|
||||
case <-time.After(globalUsageCheckInterval):
|
||||
var usage uint64
|
||||
usageFn = func(ctx context.Context, entry string) error {
|
||||
if globalHTTPServer != nil {
|
||||
// Wait at max 1 minute for an inprogress request
|
||||
// before proceeding to count the usage.
|
||||
waitCount := 60
|
||||
// Any requests in progress, delay the usage.
|
||||
for globalHTTPServer.GetRequestCount() > 0 && waitCount > 0 {
|
||||
waitCount--
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
var fi os.FileInfo
|
||||
var err error
|
||||
if hasSuffix(entry, slashSeparator) {
|
||||
@@ -230,11 +241,8 @@ func (fs *FSObjects) diskUsage(doneCh chan struct{}) {
|
||||
}
|
||||
|
||||
if err := getDiskUsage(context.Background(), fs.fsPath, usageFn); err != nil {
|
||||
atomic.StoreInt32(&fs.usageRunning, 0)
|
||||
continue
|
||||
}
|
||||
|
||||
atomic.StoreInt32(&fs.usageRunning, 0)
|
||||
atomic.StoreUint64(&fs.totalUsed, usage)
|
||||
}
|
||||
}
|
||||
@@ -242,8 +250,16 @@ func (fs *FSObjects) diskUsage(doneCh chan struct{}) {
|
||||
|
||||
// StorageInfo - returns underlying storage statistics.
|
||||
func (fs *FSObjects) StorageInfo(ctx context.Context) StorageInfo {
|
||||
di, err := getDiskInfo(fs.fsPath)
|
||||
if err != nil {
|
||||
return StorageInfo{}
|
||||
}
|
||||
used := di.Total - di.Free
|
||||
if !fs.diskMount {
|
||||
used = atomic.LoadUint64(&fs.totalUsed)
|
||||
}
|
||||
storageInfo := StorageInfo{
|
||||
Used: atomic.LoadUint64(&fs.totalUsed),
|
||||
Used: used,
|
||||
}
|
||||
storageInfo.Backend.Type = FS
|
||||
return storageInfo
|
||||
@@ -479,7 +495,7 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
||||
}
|
||||
return
|
||||
}
|
||||
// Close writer explicitly signalling we wrote all data.
|
||||
// Close writer explicitly signaling we wrote all data.
|
||||
if gerr := srcInfo.Writer.Close(); gerr != nil {
|
||||
logger.LogIf(ctx, gerr)
|
||||
return
|
||||
@@ -695,7 +711,7 @@ func (fs *FSObjects) getObjectInfoWithLock(ctx context.Context, bucket, object s
|
||||
}
|
||||
|
||||
if _, err := fs.statBucketDir(ctx, bucket); err != nil {
|
||||
return oi, toObjectErr(err, bucket)
|
||||
return oi, err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(object, slashSeparator) && !fs.isObjectDir(bucket, object) {
|
||||
@@ -952,13 +968,15 @@ func (fs *FSObjects) DeleteObject(ctx context.Context, bucket, object string) er
|
||||
// is a leaf or non-leaf entry.
|
||||
func (fs *FSObjects) listDirFactory(isLeaf isLeafFunc) listDirFunc {
|
||||
// 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))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
logger.LogIf(context.Background(), err)
|
||||
return
|
||||
}
|
||||
entries, delayIsLeaf = filterListEntries(bucket, prefixDir, entries, prefixEntry, isLeaf)
|
||||
return entries, delayIsLeaf, nil
|
||||
return entries, delayIsLeaf
|
||||
}
|
||||
|
||||
// Return list factory instance.
|
||||
@@ -1214,7 +1232,7 @@ func (fs *FSObjects) SetBucketPolicy(ctx context.Context, bucket string, policy
|
||||
|
||||
// GetBucketPolicy will get policy on bucket
|
||||
func (fs *FSObjects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
|
||||
return GetPolicyConfig(fs, bucket)
|
||||
return getPolicyConfig(fs, bucket)
|
||||
}
|
||||
|
||||
// DeleteBucketPolicy deletes all policies on bucket
|
||||
@@ -1224,7 +1242,12 @@ func (fs *FSObjects) DeleteBucketPolicy(ctx context.Context, bucket string) erro
|
||||
|
||||
// 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) {
|
||||
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 {
|
||||
return result, err
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ func FromMinioClientObjectInfo(bucket string, oi minio.ObjectInfo) ObjectInfo {
|
||||
UserDefined: userDefined,
|
||||
ContentType: oi.ContentType,
|
||||
ContentEncoding: oi.Metadata.Get("Content-Encoding"),
|
||||
StorageClass: oi.StorageClass,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-4
@@ -34,7 +34,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
logger.Init(GOPATH)
|
||||
logger.Init(GOPATH, GOROOT)
|
||||
logger.RegisterUIError(fmtError)
|
||||
}
|
||||
|
||||
@@ -159,6 +159,9 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// Initialize gateway config.
|
||||
initConfig()
|
||||
|
||||
// Load logger subsystem
|
||||
loadLoggers()
|
||||
|
||||
// Check and load SSL certificates.
|
||||
var err error
|
||||
globalPublicCerts, globalRootCAs, globalTLSCerts, globalIsSSL, err = getSSLConfig()
|
||||
@@ -170,8 +173,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
initNSLock(false) // Enable local namespace lock.
|
||||
|
||||
// Create new notification system.
|
||||
globalNotificationSys, err = NewNotificationSys(globalServerConfig, EndpointList{})
|
||||
logger.FatalIf(err, "Unable to create new notification system")
|
||||
globalNotificationSys = NewNotificationSys(globalServerConfig, EndpointList{})
|
||||
|
||||
// Create new policy system.
|
||||
globalPolicySys = NewPolicySys()
|
||||
@@ -197,7 +199,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
getCert = globalTLSCerts.GetCertificate
|
||||
}
|
||||
|
||||
globalHTTPServer = xhttp.NewServer([]string{gatewayAddr}, registerHandlers(router, globalHandlers...), getCert)
|
||||
globalHTTPServer = xhttp.NewServer([]string{gatewayAddr}, criticalErrorHandler{registerHandlers(router, globalHandlers...)}, getCert)
|
||||
globalHTTPServer.UpdateBytesReadFunc = globalConnStats.incInputBytes
|
||||
globalHTTPServer.UpdateBytesWrittenFunc = globalConnStats.incOutputBytes
|
||||
go func() {
|
||||
@@ -215,6 +217,13 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
logger.FatalIf(err, "Unable to initialize gateway backend")
|
||||
}
|
||||
|
||||
if gw.Name() != "nas" {
|
||||
// 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.
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = newObject
|
||||
|
||||
@@ -82,6 +82,7 @@ ENVIRONMENT VARIABLES:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for Azure Blob Storage backend.
|
||||
@@ -100,6 +101,7 @@ EXAMPLES:
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
`
|
||||
|
||||
@@ -238,6 +240,8 @@ func s3MetaToAzureProperties(ctx context.Context, s3Metadata map[string]string)
|
||||
props.ContentMD5 = v
|
||||
case k == "Content-Type":
|
||||
props.ContentType = v
|
||||
case k == "Content-Language":
|
||||
props.ContentLanguage = v
|
||||
}
|
||||
}
|
||||
return blobMeta, props, nil
|
||||
@@ -292,6 +296,9 @@ func azurePropertiesToS3Meta(meta storage.BlobMetadata, props storage.BlobProper
|
||||
if props.ContentType != "" {
|
||||
s3Metadata["Content-Type"] = props.ContentType
|
||||
}
|
||||
if props.ContentLanguage != "" {
|
||||
s3Metadata["Content-Language"] = props.ContentLanguage
|
||||
}
|
||||
return s3Metadata
|
||||
}
|
||||
|
||||
@@ -476,7 +483,6 @@ func (a *azureObjects) GetBucketInfo(ctx context.Context, bucket string) (bi min
|
||||
} // else continue
|
||||
}
|
||||
}
|
||||
logger.LogIf(ctx, minio.BucketNotFound{Bucket: bucket})
|
||||
return bi, minio.BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
|
||||
@@ -604,7 +610,7 @@ func (a *azureObjects) ListObjects(ctx context.Context, bucket, prefix, marker,
|
||||
// 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) {
|
||||
marker := continuationToken
|
||||
if startAfter != "" {
|
||||
if marker == "" {
|
||||
marker = startAfter
|
||||
}
|
||||
|
||||
@@ -1124,7 +1130,6 @@ func (a *azureObjects) GetBucketPolicy(ctx context.Context, bucket string) (*pol
|
||||
}
|
||||
|
||||
if perm.AccessType == storage.ContainerAccessTypePrivate {
|
||||
logger.LogIf(ctx, minio.BucketPolicyNotFound{Bucket: bucket})
|
||||
return nil, minio.BucketPolicyNotFound{Bucket: bucket}
|
||||
} else if perm.AccessType != storage.ContainerAccessTypeContainer {
|
||||
logger.LogIf(ctx, minio.NotImplemented{})
|
||||
|
||||
@@ -72,6 +72,7 @@ ENVIRONMENT VARIABLES:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for B2 backend.
|
||||
@@ -85,6 +86,7 @@ EXAMPLES:
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
`
|
||||
minio.RegisterGatewayCommand(cli.Command{
|
||||
@@ -276,7 +278,6 @@ func (l *b2Objects) Bucket(ctx context.Context, bucket string) (*b2.Bucket, erro
|
||||
return bkt, nil
|
||||
}
|
||||
}
|
||||
logger.LogIf(ctx, minio.BucketNotFound{Bucket: bucket})
|
||||
return nil, minio.BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
|
||||
@@ -353,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.
|
||||
func (l *b2Objects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int,
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
logger.LogIf(ctx, lerr)
|
||||
return loi, b2ToObjectError(lerr, bucket)
|
||||
@@ -770,7 +779,6 @@ func (l *b2Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy
|
||||
// just return back as policy not found for all cases.
|
||||
// CreateBucket always sets the value to allPrivate by default.
|
||||
if bkt.Type != bucketTypeReadOnly {
|
||||
logger.LogIf(ctx, minio.BucketPolicyNotFound{Bucket: bucket})
|
||||
return nil, minio.BucketPolicyNotFound{Bucket: bucket}
|
||||
}
|
||||
|
||||
|
||||
+108
-107
@@ -118,6 +118,7 @@ ENVIRONMENT VARIABLES:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
GCS credentials file:
|
||||
GOOGLE_APPLICATION_CREDENTIALS: Path to credentials.json
|
||||
@@ -137,6 +138,7 @@ EXAMPLES:
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}} mygcsprojectid
|
||||
`
|
||||
|
||||
@@ -552,16 +554,16 @@ func isGCSMarker(marker string) bool {
|
||||
|
||||
// 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) {
|
||||
if maxKeys == 0 {
|
||||
return minio.ListObjectsInfo{}, nil
|
||||
}
|
||||
|
||||
it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{
|
||||
Delimiter: delimiter,
|
||||
Prefix: prefix,
|
||||
Versions: false,
|
||||
})
|
||||
|
||||
isTruncated := false
|
||||
nextMarker := ""
|
||||
prefixes := []string{}
|
||||
|
||||
// To accommodate S3-compatible applications using
|
||||
// ListObjectsV1 to use object keys as markers to control the
|
||||
// listing of objects, we use the following encoding scheme to
|
||||
@@ -572,83 +574,86 @@ func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix stri
|
||||
// prefixing "{minio}" to the GCS continuation token,
|
||||
// e.g, "{minio}CgRvYmoz"
|
||||
//
|
||||
// - Application supplied markers are used as-is to list
|
||||
// object keys that appear after it in the lexicographical order.
|
||||
// - Application supplied markers are transformed to a
|
||||
// GCS continuation token.
|
||||
|
||||
// If application is using GCS continuation token we should
|
||||
// strip the gcsTokenPrefix we added.
|
||||
gcsMarker := isGCSMarker(marker)
|
||||
if gcsMarker {
|
||||
it.PageInfo().Token = strings.TrimPrefix(marker, gcsTokenPrefix)
|
||||
token := ""
|
||||
if marker != "" {
|
||||
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 {
|
||||
if len(objects) >= maxKeys {
|
||||
// check if there is one next object and
|
||||
// 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
|
||||
}
|
||||
gcsObjects := make([]*storage.ObjectAttrs, 0)
|
||||
nextPageToken, err = pager.NextPage(&gcsObjects)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ListObjectsInfo{}, gcsToObjectError(err, bucket, prefix)
|
||||
}
|
||||
|
||||
nextMarker = toGCSPageToken(attrs.Name)
|
||||
for _, attrs := range gcsObjects {
|
||||
|
||||
if attrs.Prefix == minio.GatewayMinioSysTmp {
|
||||
// We don't return our metadata prefix.
|
||||
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) {
|
||||
// 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
|
||||
// allows the response to include less keys than maxKeys.
|
||||
if attrs.Prefix == minio.GatewayMinioSysTmp {
|
||||
// We don't return our metadata prefix.
|
||||
continue
|
||||
}
|
||||
}
|
||||
if attrs.Prefix != "" {
|
||||
prefixes = append(prefixes, attrs.Prefix)
|
||||
continue
|
||||
}
|
||||
if !gcsMarker && attrs.Name <= marker {
|
||||
// if user supplied a marker don't append
|
||||
// objects until we reach marker (and skip it).
|
||||
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))
|
||||
}
|
||||
|
||||
// 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{
|
||||
Name: attrs.Name,
|
||||
Bucket: attrs.Bucket,
|
||||
ModTime: attrs.Updated,
|
||||
Size: attrs.Size,
|
||||
ETag: minio.ToS3ETag(fmt.Sprintf("%d", attrs.CRC32C)),
|
||||
UserDefined: attrs.Metadata,
|
||||
ContentType: attrs.ContentType,
|
||||
ContentEncoding: attrs.ContentEncoding,
|
||||
})
|
||||
// Exit the loop if at least one item can be returned from
|
||||
// the current page or there are no more pages available
|
||||
if nextPageToken == "" || len(prefixes)+len(objects) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nextPageToken == "" {
|
||||
nextMarker = ""
|
||||
} else if nextMarker != "" {
|
||||
nextMarker = gcsTokenPrefix + toGCSPageToken(nextMarker)
|
||||
}
|
||||
|
||||
return minio.ListObjectsInfo{
|
||||
IsTruncated: isTruncated,
|
||||
NextMarker: gcsTokenPrefix + nextMarker,
|
||||
IsTruncated: nextPageToken != "",
|
||||
NextMarker: nextMarker,
|
||||
Prefixes: prefixes,
|
||||
Objects: objects,
|
||||
}, nil
|
||||
@@ -656,68 +661,72 @@ func (l *gcsGateway) ListObjects(ctx context.Context, bucket string, prefix stri
|
||||
|
||||
// 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) {
|
||||
if maxKeys == 0 {
|
||||
return minio.ListObjectsV2Info{ContinuationToken: continuationToken}, nil
|
||||
}
|
||||
|
||||
it := l.client.Bucket(bucket).Objects(l.ctx, &storage.Query{
|
||||
Delimiter: delimiter,
|
||||
Prefix: prefix,
|
||||
Versions: false,
|
||||
})
|
||||
|
||||
isTruncated := false
|
||||
it.PageInfo().MaxSize = maxKeys
|
||||
|
||||
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
|
||||
}
|
||||
token := continuationToken
|
||||
if token == "" && startAfter != "" {
|
||||
token = toGCSPageToken(startAfter)
|
||||
}
|
||||
|
||||
var prefixes []string
|
||||
var objects []minio.ObjectInfo
|
||||
var nextPageToken string
|
||||
var err error
|
||||
|
||||
pager := iterator.NewPager(it, maxKeys, token)
|
||||
for {
|
||||
attrs, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
|
||||
gcsObjects := make([]*storage.ObjectAttrs, 0)
|
||||
nextPageToken, err = pager.NextPage(&gcsObjects)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return minio.ListObjectsV2Info{}, gcsToObjectError(err, bucket, prefix)
|
||||
}
|
||||
|
||||
if attrs.Prefix == minio.GatewayMinioSysTmp {
|
||||
// We don't return our metadata prefix.
|
||||
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) {
|
||||
for _, attrs := range gcsObjects {
|
||||
|
||||
// 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
|
||||
// allows the response to include less keys than maxKeys.
|
||||
if attrs.Prefix == minio.GatewayMinioSysTmp {
|
||||
// We don't return our metadata prefix.
|
||||
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 != "" {
|
||||
prefixes = append(prefixes, attrs.Prefix)
|
||||
continue
|
||||
// Exit the loop if at least one item can be returned from
|
||||
// the current page or there are no more pages available
|
||||
if nextPageToken == "" || len(prefixes)+len(objects) > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
objects = append(objects, fromGCSAttrsToObjectInfo(attrs))
|
||||
}
|
||||
|
||||
return minio.ListObjectsV2Info{
|
||||
IsTruncated: isTruncated,
|
||||
IsTruncated: nextPageToken != "",
|
||||
ContinuationToken: continuationToken,
|
||||
NextContinuationToken: continuationToken,
|
||||
NextContinuationToken: nextPageToken,
|
||||
Prefixes: prefixes,
|
||||
Objects: objects,
|
||||
}, nil
|
||||
@@ -1009,12 +1018,8 @@ func (l *gcsGateway) AbortMultipartUpload(ctx context.Context, bucket string, ke
|
||||
|
||||
// CompleteMultipartUpload completes ongoing multipart upload and finalizes object
|
||||
// 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
|
||||
// number of components for a given composite object. This means you can append to
|
||||
// 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.
|
||||
// be composed in a single operation. There is a per-project rate limit (currently 200)
|
||||
// to the number of source objects you can compose per second.
|
||||
func (l *gcsGateway) CompleteMultipartUpload(ctx context.Context, bucket string, key string, uploadID string, uploadedParts []minio.CompletePart) (minio.ObjectInfo, error) {
|
||||
meta := gcsMultipartMetaName(uploadID)
|
||||
object := l.client.Bucket(bucket).Object(meta)
|
||||
@@ -1134,7 +1139,6 @@ func (l *gcsGateway) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, bucketPolicy *policy.Policy) error {
|
||||
policyInfo, err := minio.PolicyToBucketAccessPolicy(bucketPolicy)
|
||||
if err != nil {
|
||||
// This should not happen.
|
||||
logger.LogIf(ctx, err)
|
||||
return gcsToObjectError(err, bucket)
|
||||
}
|
||||
@@ -1190,7 +1194,6 @@ func (l *gcsGateway) SetBucketPolicy(ctx context.Context, bucket string, bucketP
|
||||
func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
|
||||
rules, err := l.client.Bucket(bucket).ACL().List(l.ctx)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, gcsToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
@@ -1225,7 +1228,6 @@ func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*polic
|
||||
|
||||
// Return NoSuchBucketPolicy error, when policy is not set
|
||||
if len(actionSet) == 0 {
|
||||
logger.LogIf(ctx, minio.BucketPolicyNotFound{})
|
||||
return nil, gcsToObjectError(minio.BucketPolicyNotFound{}, bucket)
|
||||
}
|
||||
|
||||
@@ -1250,7 +1252,6 @@ func (l *gcsGateway) GetBucketPolicy(ctx context.Context, bucket string) (*polic
|
||||
func (l *gcsGateway) DeleteBucketPolicy(ctx context.Context, bucket string) error {
|
||||
// This only removes the storage.AllUsers policies
|
||||
if err := l.client.Bucket(bucket).ACL().Delete(l.ctx, storage.AllUsers); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return gcsToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ ENVIRONMENT VARIABLES:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for Manta Object Storage backend.
|
||||
@@ -102,6 +103,7 @@ EXAMPLES:
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
`
|
||||
|
||||
@@ -345,6 +347,13 @@ func (t *tritonObjects) ListObjects(ctx context.Context, bucket, prefix, marker,
|
||||
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{
|
||||
DirectoryName: dirName,
|
||||
Limit: uint64(maxKeys),
|
||||
@@ -417,6 +426,18 @@ func (t *tritonObjects) ListObjectsV2(ctx context.Context, bucket, prefix, conti
|
||||
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 == "." {
|
||||
dirName = path.Join(mantaRoot, bucket)
|
||||
} else {
|
||||
@@ -426,7 +447,7 @@ func (t *tritonObjects) ListObjectsV2(ctx context.Context, bucket, prefix, conti
|
||||
input = &storage.ListDirectoryInput{
|
||||
DirectoryName: dirName,
|
||||
Limit: uint64(maxKeys),
|
||||
Marker: continuationToken,
|
||||
Marker: marker,
|
||||
}
|
||||
objs, err = t.client.Dir().List(ctx, input)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,12 +17,9 @@
|
||||
package nas
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/minio/cli"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -57,6 +54,7 @@ ENVIRONMENT VARIABLES:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for NAS backend.
|
||||
@@ -70,6 +68,7 @@ EXAMPLES:
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}} /shared/nasvol
|
||||
`
|
||||
|
||||
@@ -126,8 +125,3 @@ type nasObjects struct {
|
||||
func (l *nasObjects) IsNotificationSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetBucketPolicy will get policy on bucket
|
||||
func (l *nasObjects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
|
||||
return minio.GetPolicyConfig(l, bucket)
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ ENVIRONMENT VARIABLES:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for Aliyun OSS backend.
|
||||
@@ -92,6 +93,7 @@ EXAMPLES:
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
`
|
||||
|
||||
@@ -477,8 +479,11 @@ func ossListObjects(ctx context.Context, client *oss.Client, bucket, prefix, mar
|
||||
// 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,
|
||||
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
|
||||
if marker == "" {
|
||||
marker = startAfter
|
||||
}
|
||||
|
||||
resultV1, err := ossListObjects(ctx, client, bucket, prefix, marker, delimiter, maxKeys)
|
||||
if err != nil {
|
||||
@@ -1016,7 +1021,6 @@ func (l *ossObjects) GetBucketPolicy(ctx context.Context, bucket string) (*polic
|
||||
switch result.ACL {
|
||||
case string(oss.ACLPrivate):
|
||||
// By default, all buckets starts with a "private" policy.
|
||||
logger.LogIf(ctx, minio.BucketPolicyNotFound{})
|
||||
return nil, ossToObjectError(minio.BucketPolicyNotFound{}, bucket)
|
||||
case string(oss.ACLPublicRead):
|
||||
readOnly = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Minio Cloud Storage, (C) 2017 Minio, Inc.
|
||||
* Minio Cloud Storage, (C) 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.
|
||||
@@ -20,7 +20,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/cli"
|
||||
miniogo "github.com/minio/minio-go"
|
||||
@@ -65,6 +67,7 @@ ENVIRONMENT VARIABLES:
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio gateway server for AWS S3 backend.
|
||||
@@ -83,6 +86,7 @@ EXAMPLES:
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
`
|
||||
|
||||
@@ -97,12 +101,16 @@ EXAMPLES:
|
||||
|
||||
// Handler for 'minio gateway s3' command line.
|
||||
func s3GatewayMain(ctx *cli.Context) {
|
||||
// Validate gateway arguments.
|
||||
host := ctx.Args().First()
|
||||
// Validate gateway arguments.
|
||||
logger.FatalIf(minio.ValidateGatewayArguments(ctx.GlobalString("address"), host), "Invalid argument")
|
||||
args := ctx.Args()
|
||||
if !ctx.Args().Present() {
|
||||
args = cli.Args{"https://s3.amazonaws.com"}
|
||||
}
|
||||
|
||||
minio.StartGateway(ctx, &S3{host})
|
||||
// Validate gateway arguments.
|
||||
logger.FatalIf(minio.ValidateGatewayArguments(ctx.GlobalString("address"), args.First()), "Invalid argument")
|
||||
|
||||
// Start the gateway..
|
||||
minio.StartGateway(ctx, &S3{args.First()})
|
||||
}
|
||||
|
||||
// S3 implements Gateway.
|
||||
@@ -115,34 +123,71 @@ func (g *S3) Name() string {
|
||||
return s3Backend
|
||||
}
|
||||
|
||||
// NewGatewayLayer returns s3 ObjectLayer.
|
||||
func (g *S3) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
|
||||
var err error
|
||||
var endpoint string
|
||||
var secure = true
|
||||
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
|
||||
)
|
||||
|
||||
// Validate host parameters.
|
||||
if g.host != "" {
|
||||
// Override default params if the host is provided
|
||||
endpoint, secure, err = minio.ParseGatewayEndpoint(g.host)
|
||||
// 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.
|
||||
func newS3(url, accessKey, secretKey string) (*miniogo.Core, error) {
|
||||
if url == "" {
|
||||
url = "https://s3.amazonaws.com"
|
||||
}
|
||||
|
||||
// Override default params if the host is provided
|
||||
endpoint, secure, err := minio.ParseGatewayEndpoint(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clnt, err := miniogo.NewV4(endpoint, accessKey, secretKey, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
probeBucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "probe-bucket-sign-")
|
||||
if _, err = clnt.BucketExists(probeBucketName); err != nil {
|
||||
clnt, err = miniogo.NewV2(endpoint, accessKey, secretKey, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = clnt.BucketExists(probeBucketName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Default endpoint parameters
|
||||
if endpoint == "" {
|
||||
endpoint = "s3.amazonaws.com"
|
||||
}
|
||||
return &miniogo.Core{Client: clnt}, nil
|
||||
}
|
||||
|
||||
// Initialize minio client object.
|
||||
client, err := miniogo.NewCore(endpoint, creds.AccessKey, creds.SecretKey, secure)
|
||||
// NewGatewayLayer returns s3 ObjectLayer.
|
||||
func (g *S3) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, error) {
|
||||
// Probe S3 signature with input credentials.
|
||||
clnt, err := newS3(g.host, creds.AccessKey, creds.SecretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &s3Objects{
|
||||
Client: client,
|
||||
Client: clnt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -209,7 +254,6 @@ func (l *s3Objects) GetBucketInfo(ctx context.Context, bucket string) (bi minio.
|
||||
}, nil
|
||||
}
|
||||
|
||||
logger.LogIf(ctx, minio.BucketNotFound{Bucket: bucket})
|
||||
return bi, minio.BucketNotFound{Bucket: bucket}
|
||||
}
|
||||
|
||||
@@ -255,7 +299,7 @@ func (l *s3Objects) ListObjects(ctx context.Context, bucket string, prefix strin
|
||||
|
||||
// ListObjectsV2 lists all blobs in S3 bucket filtered by prefix
|
||||
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)
|
||||
result, err := l.Client.ListObjectsV2(bucket, prefix, continuationToken, fetchOwner, delimiter, maxKeys, startAfter)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return loi, minio.ErrorRespToObjectError(err, bucket)
|
||||
@@ -446,7 +490,6 @@ func (l *s3Objects) SetBucketPolicy(ctx context.Context, bucket string, bucketPo
|
||||
func (l *s3Objects) GetBucketPolicy(ctx context.Context, bucket string) (*policy.Policy, error) {
|
||||
data, err := l.Client.GetBucketPolicy(bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil, minio.ErrorRespToObjectError(err, bucket)
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ ENVIRONMENT VARIABLES: (Default values in parenthesis)
|
||||
MINIO_CACHE_DRIVES: List of mounted drives or directories delimited by ";".
|
||||
MINIO_CACHE_EXCLUDE: List of cache exclusion patterns delimited by ";".
|
||||
MINIO_CACHE_EXPIRY: Cache expiry duration in days.
|
||||
MINIO_CACHE_MAXUSE: Maximum permitted usage of the cache in percentage (0-100).
|
||||
|
||||
SIA_TEMP_DIR: The name of the local Sia temporary storage directory. (.sia_temp)
|
||||
SIA_API_PASSWORD: API password for Sia daemon. (default is empty)
|
||||
@@ -91,6 +92,7 @@ EXAMPLES:
|
||||
$ export MINIO_CACHE_DRIVES="/mnt/drive1;/mnt/drive2;/mnt/drive3;/mnt/drive4"
|
||||
$ export MINIO_CACHE_EXCLUDE="bucket1/*;*.png"
|
||||
$ export MINIO_CACHE_EXPIRY=40
|
||||
$ export MINIO_CACHE_MAXUSE=80
|
||||
$ {{.HelpName}}
|
||||
`
|
||||
|
||||
|
||||
+111
-2
@@ -19,13 +19,18 @@ package cmd
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/sys"
|
||||
"github.com/rs/cors"
|
||||
"golang.org/x/time/rate"
|
||||
@@ -611,14 +616,81 @@ func (h pathValidityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// To forward the path style requests on a bucket to the right
|
||||
// configured server, bucket to IP configuration is obtained
|
||||
// from centralized etcd configuration service.
|
||||
type bucketForwardingHandler struct {
|
||||
fwd *handlers.Forwarder
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if globalDNSConfig == nil || globalDomainName == "" || guessIsBrowserReq(r) || guessIsHealthCheckReq(r) || guessIsMetricsReq(r) || guessIsRPCReq(r) {
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
bucket, object := urlPath2BucketObjectName(r.URL.Path)
|
||||
// ListBucket requests should be handled at current endpoint as
|
||||
// all buckets data can be fetched from here.
|
||||
if r.Method == http.MethodGet && bucket == "" && object == "" {
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// MakeBucket requests should be handled at current endpoint
|
||||
if r.Method == http.MethodPut && bucket != "" && object == "" {
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// CopyObject requests should be handled at current endpoint as path style
|
||||
// requests have target bucket and object in URI and source details are in
|
||||
// header fields
|
||||
if r.Method == http.MethodPut && r.Header.Get("X-Amz-Copy-Source") != "" {
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
sr, err := globalDNSConfig.Get(bucket)
|
||||
if err != nil {
|
||||
if err == dns.ErrNoEntriesFound {
|
||||
writeErrorResponse(w, ErrNoSuchBucket, r.URL)
|
||||
} else {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
|
||||
host, port := getRandomHostPort(sr)
|
||||
r.URL.Scheme = "http"
|
||||
if globalIsSSL {
|
||||
r.URL.Scheme = "https"
|
||||
}
|
||||
r.URL.Host = fmt.Sprintf("%s:%d", host, port)
|
||||
f.fwd.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
f.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// setBucketForwardingHandler middleware forwards the path style requests
|
||||
// on a bucket to the right bucket location, bucket to IP configuration
|
||||
// is obtained from centralized etcd configuration service.
|
||||
func setBucketForwardingHandler(h http.Handler) http.Handler {
|
||||
fwd := handlers.NewForwarder(&handlers.Forwarder{
|
||||
PassHost: true,
|
||||
RoundTripper: NewCustomHTTPTransport(),
|
||||
})
|
||||
return bucketForwardingHandler{fwd, h}
|
||||
}
|
||||
|
||||
// setRateLimitHandler middleware limits the throughput to h using a
|
||||
// rate.Limiter token bucket configured with maxOpenFileLimit and
|
||||
// burst set to 1. The request will idle for up to 1*time.Second.
|
||||
// If the limiter detects the deadline will be exceeded, the request is
|
||||
// cancelled immediately.
|
||||
// canceled immediately.
|
||||
func setRateLimitHandler(h http.Handler) http.Handler {
|
||||
_, maxLimit, err := sys.GetMaxOpenFileLimit()
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to get maximum open file limit", context.Background())
|
||||
// Burst value is set to 1 to allow only maxOpenFileLimit
|
||||
// requests to happen at once.
|
||||
l := rate.NewLimiter(rate.Limit(maxLimit), 1)
|
||||
@@ -651,6 +723,26 @@ func (l rateLimit) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
handler http.Handler
|
||||
}
|
||||
@@ -665,3 +757,20 @@ func (s securityHeaderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
header.Set("Content-Security-Policy", "block-all-mixed-content") // prevent mixed (HTTP / HTTPS content)
|
||||
s.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// criticalErrorHandler handles critical server failures caused by
|
||||
// `panic(logger.ErrCritical)` as done by `logger.CriticalIf`.
|
||||
//
|
||||
// It should be always the first / highest HTTP handler.
|
||||
type criticalErrorHandler struct{ handler http.Handler }
|
||||
|
||||
func (h criticalErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err == logger.ErrCritical { // handle
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
} else if err != nil {
|
||||
panic(err) // forward other panic calls
|
||||
}
|
||||
}()
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
+20
-3
@@ -22,11 +22,15 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/fatih/color"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/certs"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
)
|
||||
|
||||
// minio configuration related constants.
|
||||
@@ -159,7 +163,8 @@ var (
|
||||
globalPublicCerts []*x509.Certificate
|
||||
|
||||
globalIsEnvDomainName bool
|
||||
globalDomainName string // Root domain for virtual host style requests
|
||||
globalDomainName string // Root domain for virtual host style requests
|
||||
globalDomainIPs set.StringSet // Root domain IP address(s) for a distributed Minio deployment
|
||||
|
||||
globalListingTimeout = newDynamicTimeout( /*30*/ 600*time.Second /*5*/, 600*time.Second) // timeout for listing related ops
|
||||
globalObjectTimeout = newDynamicTimeout( /*1*/ 10*time.Minute /*10*/, 600*time.Second) // timeout for Object API related ops
|
||||
@@ -174,17 +179,23 @@ var (
|
||||
// Set to store standard storage class
|
||||
globalStandardStorageClass storageClass
|
||||
|
||||
globalIsEnvWORM bool
|
||||
globalIsEnvWORM bool
|
||||
// Is worm enabled
|
||||
globalWORMEnabled bool
|
||||
|
||||
// Is Disk Caching set up
|
||||
globalIsDiskCacheEnabled bool
|
||||
|
||||
// Disk cache drives
|
||||
globalCacheDrives []string
|
||||
|
||||
// Disk cache excludes
|
||||
globalCacheExcludes []string
|
||||
|
||||
// Disk cache expiry
|
||||
globalCacheExpiry = 90
|
||||
// Max allowed disk cache percentage
|
||||
globalCacheMaxUse = 80
|
||||
|
||||
// RPC V1 - Initial version
|
||||
// RPC V2 - format.json XL version changed to 2
|
||||
@@ -192,12 +203,18 @@ var (
|
||||
// Current RPC version
|
||||
globalRPCAPIVersion = RPCVersion{3, 0, 0}
|
||||
|
||||
// Add new variable global values here.
|
||||
// Allocated etcd endpoint for config and bucket DNS.
|
||||
globalEtcdClient *etcd.Client
|
||||
|
||||
// Allocated DNS config wrapper over etcd client.
|
||||
globalDNSConfig dns.Config
|
||||
|
||||
// Default usage check interval value.
|
||||
globalDefaultUsageCheckInterval = 12 * time.Hour // 12 hours
|
||||
// Usage check interval value.
|
||||
globalUsageCheckInterval = globalDefaultUsageCheckInterval
|
||||
|
||||
// Add new variable global values here.
|
||||
)
|
||||
|
||||
// global colors.
|
||||
|
||||
+39
-24
@@ -26,6 +26,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
httptracer "github.com/minio/minio/pkg/handlers"
|
||||
)
|
||||
|
||||
@@ -113,40 +114,54 @@ var userMetadataKeyPrefixes = []string{
|
||||
"X-Minio-Meta-",
|
||||
}
|
||||
|
||||
// extractMetadataFromHeader extracts metadata from HTTP header.
|
||||
func extractMetadataFromHeader(ctx context.Context, header http.Header) (map[string]string, error) {
|
||||
if header == nil {
|
||||
logger.LogIf(ctx, errInvalidArgument)
|
||||
return nil, errInvalidArgument
|
||||
// extractMetadata extracts metadata from HTTP header and HTTP queryString.
|
||||
func extractMetadata(ctx context.Context, r *http.Request) (metadata map[string]string, err error) {
|
||||
query := r.URL.Query()
|
||||
header := r.Header
|
||||
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.
|
||||
for _, supportedHeader := range supportedHeaders {
|
||||
canonicalHeader := http.CanonicalHeaderKey(supportedHeader)
|
||||
// HTTP headers are case insensitive, look for both canonical
|
||||
// and non canonical entries.
|
||||
if _, ok := header[canonicalHeader]; ok {
|
||||
metadata[supportedHeader] = header.Get(canonicalHeader)
|
||||
} else if _, ok := header[supportedHeader]; ok {
|
||||
metadata[supportedHeader] = header.Get(supportedHeader)
|
||||
if value, ok := v[http.CanonicalHeaderKey(supportedHeader)]; ok {
|
||||
m[supportedHeader] = value[0]
|
||||
} else if value, ok := v[supportedHeader]; ok {
|
||||
m[supportedHeader] = value[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 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 key := range v {
|
||||
for _, prefix := range userMetadataKeyPrefixes {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
metadata[key] = header.Get(key)
|
||||
if !strings.HasPrefix(strings.ToLower(key), strings.ToLower(prefix)) {
|
||||
continue
|
||||
}
|
||||
value, ok := v[key]
|
||||
if ok {
|
||||
m[key] = strings.Join(value, ",")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return metadata, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// The Query string for the redirect URL the client is
|
||||
@@ -167,7 +182,7 @@ func extractReqParams(r *http.Request) map[string]string {
|
||||
|
||||
// Success.
|
||||
return map[string]string{
|
||||
"sourceIPAddress": r.RemoteAddr,
|
||||
"sourceIPAddress": handlers.GetSourceIP(r),
|
||||
// Add more fields here.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,9 +165,9 @@ func TestExtractMetadataHeaders(t *testing.T) {
|
||||
"x-amz-meta-appid": []string{"amz-meta"},
|
||||
},
|
||||
metadata: map[string]string{
|
||||
"X-Amz-Meta-Appid": "amz-meta",
|
||||
"x-amz-meta-appid": "amz-meta",
|
||||
},
|
||||
shouldFail: true,
|
||||
shouldFail: false,
|
||||
},
|
||||
// Empty header input returns empty metadata.
|
||||
{
|
||||
@@ -179,7 +179,8 @@ func TestExtractMetadataHeaders(t *testing.T) {
|
||||
|
||||
// Validate if the extracting headers.
|
||||
for i, testCase := range testCases {
|
||||
metadata, err := extractMetadataFromHeader(context.Background(), testCase.header)
|
||||
metadata := make(map[string]string)
|
||||
err := extractMetadataFromMap(context.Background(), testCase.header, metadata)
|
||||
if err != nil && !testCase.shouldFail {
|
||||
t.Fatalf("Test %d failed to extract metadata: %v", i+1, err)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -40,19 +42,38 @@ func ReadinessCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
writeResponse(w, http.StatusOK, nil, mimeNone)
|
||||
}
|
||||
|
||||
// LivenessCheckHandler -- checks if server can ListBuckets internally. If not, server is
|
||||
// considered to have failed and needs to be restarted.
|
||||
// LivenessCheckHandler -- checks if server can reach its disks internally.
|
||||
// If not, server is considered to have failed and needs to be restarted.
|
||||
// Liveness probes are used to detect situations where application (minio)
|
||||
// has gone into a state where it can not recover except by being restarted.
|
||||
func LivenessCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "LivenessCheckHandler")
|
||||
|
||||
objLayer := newObjectLayerFn()
|
||||
// Service not initialized yet
|
||||
if objLayer == nil {
|
||||
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
||||
return
|
||||
}
|
||||
// List buckets is unsuccessful, means server is having issues, send 503 service unavailable
|
||||
if _, err := objLayer.ListBuckets(context.Background()); err != nil {
|
||||
var totalLocalDisks int
|
||||
var erroredDisks int
|
||||
for _, endpoint := range globalEndpoints {
|
||||
// Check only if local disks are accessible, we do not have
|
||||
// to reach to rest of the other servers in a distributed setup.
|
||||
if endpoint.IsLocal {
|
||||
totalLocalDisks++
|
||||
// Attempt a stat to backend, any error resulting
|
||||
// from this Stat() operation is considered as backend
|
||||
// is not available, count them as errors.
|
||||
if _, err := os.Stat(endpoint.Path); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
erroredDisks++
|
||||
}
|
||||
}
|
||||
}
|
||||
// If all exported local disks have errored, we simply let kubernetes
|
||||
// take us down.
|
||||
if totalLocalDisks == erroredDisks {
|
||||
writeResponse(w, http.StatusServiceUnavailable, nil, mimeNone)
|
||||
return
|
||||
}
|
||||
|
||||
+6
-1
@@ -59,7 +59,12 @@ type Server struct {
|
||||
listenerMutex *sync.Mutex // to guard 'listener' field.
|
||||
listener *httpListener // HTTP listener for all 'Addrs' field.
|
||||
inShutdown uint32 // indicates whether the server is in shutdown or not
|
||||
requestCount int32 // counter holds no. of request in process.
|
||||
requestCount int32 // counter holds no. of request in progress.
|
||||
}
|
||||
|
||||
// GetRequestCount - returns number of request in progress.
|
||||
func (srv *Server) GetRequestCount() int32 {
|
||||
return atomic.LoadInt32(&srv.requestCount)
|
||||
}
|
||||
|
||||
// Start - start HTTP server
|
||||
|
||||
@@ -124,7 +124,10 @@ func (l *lockRPCReceiver) lockMaintenance(interval time.Duration) {
|
||||
for _, nlrip := range nlripLongLived {
|
||||
// Initialize client based on the long live locks.
|
||||
host, err := xnet.ParseHost(nlrip.lri.node)
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
c, err := NewLockRPCClient(host)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
@@ -183,7 +186,7 @@ func NewLockRPCServer() (*xrpc.Server, error) {
|
||||
// Register distributed NS lock handlers.
|
||||
func registerDistNSLockRouter(router *mux.Router) {
|
||||
rpcServer, err := NewLockRPCServer()
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to initialize Lock RPC Server", context.Background())
|
||||
|
||||
// Start lock maintenance from all lock servers.
|
||||
go startLockMaintenance(globalLockServer)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConsoleTarget implements loggerTarget to send log
|
||||
// in plain or json format to the standard output.
|
||||
type ConsoleTarget struct{}
|
||||
|
||||
func (c *ConsoleTarget) send(entry logEntry) error {
|
||||
if jsonFlag {
|
||||
logJSON, err := json.Marshal(&entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(logJSON))
|
||||
return nil
|
||||
}
|
||||
|
||||
trace := make([]string, len(entry.Trace.Source))
|
||||
|
||||
// Add a sequence number and formatting for each stack trace
|
||||
// No formatting is required for the first entry
|
||||
for i, element := range entry.Trace.Source {
|
||||
trace[i] = fmt.Sprintf("%8v: %s", i+1, element)
|
||||
}
|
||||
|
||||
tagString := ""
|
||||
for key, value := range entry.Trace.Variables {
|
||||
if value != "" {
|
||||
if tagString != "" {
|
||||
tagString += ", "
|
||||
}
|
||||
tagString += key + "=" + value
|
||||
}
|
||||
}
|
||||
|
||||
apiString := "API: " + entry.API.Name + "("
|
||||
if entry.API.Args != nil && entry.API.Args.Bucket != "" {
|
||||
apiString = apiString + "bucket=" + entry.API.Args.Bucket
|
||||
}
|
||||
if entry.API.Args != nil && entry.API.Args.Object != "" {
|
||||
apiString = apiString + ", object=" + entry.API.Args.Object
|
||||
}
|
||||
apiString += ")"
|
||||
timeString := "Time: " + time.Now().Format(loggerTimeFormat)
|
||||
|
||||
var requestID string
|
||||
if entry.RequestID != "" {
|
||||
requestID = "\nRequestID: " + entry.RequestID
|
||||
}
|
||||
|
||||
var remoteHost string
|
||||
if entry.RemoteHost != "" {
|
||||
remoteHost = "\nRemoteHost: " + entry.RemoteHost
|
||||
}
|
||||
|
||||
var userAgent string
|
||||
if entry.UserAgent != "" {
|
||||
userAgent = "\nUserAgent: " + entry.UserAgent
|
||||
}
|
||||
|
||||
if len(entry.Trace.Variables) > 0 {
|
||||
tagString = "\n " + tagString
|
||||
}
|
||||
|
||||
var msg = colorFgRed(colorBold(entry.Trace.Message))
|
||||
var output = fmt.Sprintf("\n%s\n%s%s%s%s\nError: %s%s\n%s",
|
||||
apiString, timeString, requestID, remoteHost, userAgent,
|
||||
msg, tagString, strings.Join(trace, "\n"))
|
||||
|
||||
fmt.Println(output)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewConsole initializes a new logger target
|
||||
// which prints log directly in the standard
|
||||
// output.
|
||||
func NewConsole() LoggingTarget {
|
||||
return &ConsoleTarget{}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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 logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HTTPTarget implements loggerTarget and sends the json
|
||||
// format of a log entry to the configured http endpoint.
|
||||
// An internal buffer of logs is maintained but when the
|
||||
// buffer is full, new logs are just ignored and an error
|
||||
// is returned to the caller.
|
||||
type HTTPTarget struct {
|
||||
// Channel of log entries
|
||||
logCh chan logEntry
|
||||
// HTTP(s) endpoint
|
||||
endpoint string
|
||||
client http.Client
|
||||
}
|
||||
|
||||
func (h *HTTPTarget) startHTTPLogger() {
|
||||
// Create a routine which sends json logs received
|
||||
// from an internal channel.
|
||||
go func() {
|
||||
for entry := range h.logCh {
|
||||
logJSON, err := json.Marshal(&entry)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", h.endpoint, bytes.NewBuffer(logJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// NewHTTP initializes a new logger target which
|
||||
// sends log over http to the specified endpoint
|
||||
func NewHTTP(endpoint string, transport *http.Transport) LoggingTarget {
|
||||
h := HTTPTarget{
|
||||
endpoint: endpoint,
|
||||
client: http.Client{
|
||||
Transport: transport,
|
||||
},
|
||||
logCh: make(chan logEntry, 10000),
|
||||
}
|
||||
|
||||
h.startHTTPLogger()
|
||||
return &h
|
||||
}
|
||||
|
||||
func (h *HTTPTarget) send(entry logEntry) error {
|
||||
select {
|
||||
case h.logCh <- entry:
|
||||
default:
|
||||
// log channel is full, do not wait and return
|
||||
// an error immediately to the caller
|
||||
return errors.New("log buffer full")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+83
-108
@@ -28,7 +28,6 @@ import (
|
||||
"time"
|
||||
|
||||
c "github.com/minio/mc/pkg/console"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// Disable disables all logging, false by default. (used for "go test")
|
||||
@@ -52,6 +51,25 @@ var matchingFuncNames = [...]string{
|
||||
"http.HandlerFunc.ServeHTTP",
|
||||
"cmd.serverMain",
|
||||
"cmd.StartGateway",
|
||||
"cmd.(*webAPIHandlers).ListBuckets",
|
||||
"cmd.(*webAPIHandlers).MakeBucket",
|
||||
"cmd.(*webAPIHandlers).DeleteBucket",
|
||||
"cmd.(*webAPIHandlers).ListObjects",
|
||||
"cmd.(*webAPIHandlers).RemoveObject",
|
||||
"cmd.(*webAPIHandlers).Login",
|
||||
"cmd.(*webAPIHandlers).GenerateAuth",
|
||||
"cmd.(*webAPIHandlers).SetAuth",
|
||||
"cmd.(*webAPIHandlers).GetAuth",
|
||||
"cmd.(*webAPIHandlers).CreateURLToken",
|
||||
"cmd.(*webAPIHandlers).Upload",
|
||||
"cmd.(*webAPIHandlers).Download",
|
||||
"cmd.(*webAPIHandlers).DownloadZip",
|
||||
"cmd.(*webAPIHandlers).GetBucketPolicy",
|
||||
"cmd.(*webAPIHandlers).ListAllBucketPolicies",
|
||||
"cmd.(*webAPIHandlers).SetBucketPolicy",
|
||||
"cmd.(*webAPIHandlers).PresignedGet",
|
||||
"cmd.(*webAPIHandlers).ServerInfo",
|
||||
"cmd.(*webAPIHandlers).StorageInfo",
|
||||
// add more here ..
|
||||
}
|
||||
|
||||
@@ -103,14 +121,15 @@ type api struct {
|
||||
}
|
||||
|
||||
type logEntry struct {
|
||||
Level string `json:"level"`
|
||||
Time string `json:"time"`
|
||||
API *api `json:"api,omitempty"`
|
||||
RemoteHost string `json:"remotehost,omitempty"`
|
||||
RequestID string `json:"requestID,omitempty"`
|
||||
UserAgent string `json:"userAgent,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Trace *traceEntry `json:"error,omitempty"`
|
||||
DeploymentID string `json:"deploymentid,omitempty"`
|
||||
Level string `json:"level"`
|
||||
Time string `json:"time"`
|
||||
API *api `json:"api,omitempty"`
|
||||
RemoteHost string `json:"remotehost,omitempty"`
|
||||
RequestID string `json:"requestID,omitempty"`
|
||||
UserAgent string `json:"userAgent,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Trace *traceEntry `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// quiet: Hide startup messages if enabled
|
||||
@@ -119,8 +138,15 @@ var (
|
||||
quiet, jsonFlag bool
|
||||
// Custom function to format error
|
||||
errorFmtFunc func(string, error, bool) string
|
||||
|
||||
deploymentID string
|
||||
)
|
||||
|
||||
// SetDeploymentID - Used to set the deployment ID, in XL and FS mode
|
||||
func SetDeploymentID(id string) {
|
||||
deploymentID = id
|
||||
}
|
||||
|
||||
// EnableQuiet - turns quiet option on.
|
||||
func EnableQuiet() {
|
||||
quiet = true
|
||||
@@ -142,22 +168,24 @@ func RegisterUIError(f func(string, error, bool) string) {
|
||||
// and GOROOT directories. Also append github.com/minio/minio
|
||||
// This is done to clean up the filename, when stack trace is
|
||||
// displayed when an error happens.
|
||||
func Init(goPath string) {
|
||||
func Init(goPath string, goRoot string) {
|
||||
|
||||
var goPathList []string
|
||||
var goRootList []string
|
||||
var defaultgoPathList []string
|
||||
var defaultgoRootList []string
|
||||
pathSeperator := ":"
|
||||
// Add all possible GOPATH paths into trimStrings
|
||||
// Split GOPATH depending on the OS type
|
||||
if runtime.GOOS == "windows" {
|
||||
goPathList = strings.Split(goPath, ";")
|
||||
defaultgoPathList = strings.Split(build.Default.GOPATH, ";")
|
||||
} else {
|
||||
// All other types of OSs
|
||||
goPathList = strings.Split(goPath, ":")
|
||||
defaultgoPathList = strings.Split(build.Default.GOPATH, ":")
|
||||
|
||||
pathSeperator = ";"
|
||||
}
|
||||
|
||||
goPathList = strings.Split(goPath, pathSeperator)
|
||||
goRootList = strings.Split(goRoot, pathSeperator)
|
||||
defaultgoPathList = strings.Split(build.Default.GOPATH, pathSeperator)
|
||||
defaultgoRootList = strings.Split(build.Default.GOROOT, pathSeperator)
|
||||
|
||||
// Add trim string "{GOROOT}/src/" into trimStrings
|
||||
trimStrings = []string{filepath.Join(runtime.GOROOT(), "src") + string(filepath.Separator)}
|
||||
|
||||
@@ -167,10 +195,21 @@ func Init(goPath string) {
|
||||
trimStrings = append(trimStrings, filepath.Join(goPathString, "src")+string(filepath.Separator))
|
||||
}
|
||||
|
||||
for _, goRootString := range goRootList {
|
||||
trimStrings = append(trimStrings, filepath.Join(goRootString, "src")+string(filepath.Separator))
|
||||
}
|
||||
|
||||
for _, defaultgoPathString := range defaultgoPathList {
|
||||
trimStrings = append(trimStrings, filepath.Join(defaultgoPathString, "src")+string(filepath.Separator))
|
||||
}
|
||||
|
||||
for _, defaultgoRootString := range defaultgoRootList {
|
||||
trimStrings = append(trimStrings, filepath.Join(defaultgoRootString, "src")+string(filepath.Separator))
|
||||
}
|
||||
|
||||
// Remove duplicate entries.
|
||||
trimStrings = uniqueEntries(trimStrings)
|
||||
|
||||
// Add "github.com/minio/minio" as the last to cover
|
||||
// paths like "{GOROOT}/src/github.com/minio/minio"
|
||||
// and "{GOPATH}/src/github.com/minio/minio"
|
||||
@@ -200,7 +239,7 @@ func getTrace(traceLevel int) []string {
|
||||
var trace []string
|
||||
pc, file, lineNumber, ok := runtime.Caller(traceLevel)
|
||||
|
||||
for ok {
|
||||
for ok && file != "" {
|
||||
// Clean up the common prefixes
|
||||
file = trimTrace(file)
|
||||
// Get the function name
|
||||
@@ -260,83 +299,32 @@ func LogIf(ctx context.Context, err error) {
|
||||
// Get the cause for the Error
|
||||
message := err.Error()
|
||||
|
||||
// Output the formatted log message at console
|
||||
var output string
|
||||
if jsonFlag {
|
||||
logJSON, err := json.Marshal(&logEntry{
|
||||
Level: ErrorLvl.String(),
|
||||
RemoteHost: req.RemoteHost,
|
||||
RequestID: req.RequestID,
|
||||
UserAgent: req.UserAgent,
|
||||
Time: time.Now().UTC().Format(time.RFC3339Nano),
|
||||
API: &api{Name: API, Args: &args{Bucket: req.BucketName, Object: req.ObjectName}},
|
||||
Trace: &traceEntry{Message: message, Source: trace, Variables: tags},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
output = string(logJSON)
|
||||
} else {
|
||||
// Add a sequence number and formatting for each stack trace
|
||||
// No formatting is required for the first entry
|
||||
for i, element := range trace {
|
||||
trace[i] = fmt.Sprintf("%8v: %s", i+1, element)
|
||||
}
|
||||
|
||||
tagString := ""
|
||||
for key, value := range tags {
|
||||
if value != "" {
|
||||
if tagString != "" {
|
||||
tagString += ", "
|
||||
}
|
||||
tagString += key + "=" + value
|
||||
}
|
||||
}
|
||||
|
||||
apiString := "API: " + API + "("
|
||||
if req.BucketName != "" {
|
||||
apiString = apiString + "bucket=" + req.BucketName
|
||||
}
|
||||
if req.ObjectName != "" {
|
||||
apiString = apiString + ", object=" + req.ObjectName
|
||||
}
|
||||
apiString += ")"
|
||||
timeString := "Time: " + time.Now().Format(loggerTimeFormat)
|
||||
|
||||
var requestID string
|
||||
if req.RequestID != "" {
|
||||
requestID = "\nRequestID: " + req.RequestID
|
||||
}
|
||||
|
||||
var remoteHost string
|
||||
if req.RemoteHost != "" {
|
||||
remoteHost = "\nRemoteHost: " + req.RemoteHost
|
||||
}
|
||||
|
||||
var userAgent string
|
||||
if req.UserAgent != "" {
|
||||
userAgent = "\nUserAgent: " + req.UserAgent
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
tagString = "\n " + tagString
|
||||
}
|
||||
|
||||
var msg = colorFgRed(colorBold(message))
|
||||
output = fmt.Sprintf("\n%s\n%s%s%s%s\nError: %s%s\n%s",
|
||||
apiString, timeString, requestID, remoteHost, userAgent,
|
||||
msg, tagString, strings.Join(trace, "\n"))
|
||||
entry := logEntry{
|
||||
DeploymentID: deploymentID,
|
||||
Level: ErrorLvl.String(),
|
||||
RemoteHost: req.RemoteHost,
|
||||
RequestID: req.RequestID,
|
||||
UserAgent: req.UserAgent,
|
||||
Time: time.Now().UTC().Format(time.RFC3339Nano),
|
||||
API: &api{Name: API, Args: &args{Bucket: req.BucketName, Object: req.ObjectName}},
|
||||
Trace: &traceEntry{Message: message, Source: trace, Variables: tags},
|
||||
}
|
||||
|
||||
// Iterate over all logger targets to send the log entry
|
||||
for _, t := range Targets {
|
||||
t.send(entry)
|
||||
}
|
||||
fmt.Println(output)
|
||||
}
|
||||
|
||||
// CriticalIf :
|
||||
// Like LogIf with exit
|
||||
// It'll be called for fatal error conditions during run-time
|
||||
// ErrCritical is the value panic'd whenever CriticalIf is called.
|
||||
var ErrCritical struct{}
|
||||
|
||||
// CriticalIf logs the provided error on the console. It fails the
|
||||
// current go-routine by causing a `panic(ErrCritical)`.
|
||||
func CriticalIf(ctx context.Context, err error) {
|
||||
if err != nil {
|
||||
LogIf(ctx, err)
|
||||
os.Exit(1)
|
||||
panic(ErrCritical)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,23 +377,15 @@ func (f fatalMsg) quiet(msg string, args ...interface{}) {
|
||||
}
|
||||
|
||||
var (
|
||||
logTag = "ERROR"
|
||||
logBanner = colorBgRed(colorFgWhite(colorBold(logTag))) + " "
|
||||
emptyBanner = colorBgRed(strings.Repeat(" ", len(logTag))) + " "
|
||||
minimumWidth = 80
|
||||
bannerWidth = len(logTag) + 1
|
||||
logTag = "ERROR"
|
||||
logBanner = colorBgRed(colorFgWhite(colorBold(logTag))) + " "
|
||||
emptyBanner = colorBgRed(strings.Repeat(" ", len(logTag))) + " "
|
||||
bannerWidth = len(logTag) + 1
|
||||
)
|
||||
|
||||
func (f fatalMsg) pretty(msg string, args ...interface{}) {
|
||||
// Build the passed error message
|
||||
errMsg := fmt.Sprintf(msg, args...)
|
||||
// Check terminal width
|
||||
termWidth, _, err := terminal.GetSize(0)
|
||||
if err != nil || termWidth < minimumWidth {
|
||||
termWidth = minimumWidth
|
||||
}
|
||||
// Calculate available widht without the banner
|
||||
width := termWidth - bannerWidth
|
||||
|
||||
tagPrinted := false
|
||||
|
||||
@@ -436,13 +416,8 @@ func (f fatalMsg) pretty(msg string, args ...interface{}) {
|
||||
ansiRestoreAttributes()
|
||||
ansiMoveRight(bannerWidth)
|
||||
// Continue error message printing
|
||||
if len(line) > width {
|
||||
fmt.Println(line[:width])
|
||||
line = line[width:]
|
||||
} else {
|
||||
fmt.Println(line)
|
||||
break
|
||||
}
|
||||
fmt.Println(line)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 logger
|
||||
|
||||
// LoggingTarget is the entity that we will receive
|
||||
// a single log entry and send it to the log target
|
||||
// e.g. send the log to a http server
|
||||
type LoggingTarget interface {
|
||||
send(entry logEntry) error
|
||||
}
|
||||
|
||||
// Targets is the set of enabled loggers
|
||||
var Targets = []LoggingTarget{}
|
||||
|
||||
// AddTarget adds a new logger target to the
|
||||
// list of enabled loggers
|
||||
func AddTarget(t LoggingTarget) {
|
||||
Targets = append(Targets, t)
|
||||
}
|
||||
@@ -50,3 +50,16 @@ func ansiSaveAttributes() {
|
||||
func ansiRestoreAttributes() {
|
||||
ansiEscape("8")
|
||||
}
|
||||
|
||||
func uniqueEntries(paths []string) []string {
|
||||
found := map[string]bool{}
|
||||
unqiue := []string{}
|
||||
|
||||
for v := range paths {
|
||||
if _, ok := found[paths[v]]; !ok {
|
||||
found[paths[v]] = true
|
||||
unqiue = append(unqiue, paths[v])
|
||||
}
|
||||
}
|
||||
return unqiue
|
||||
}
|
||||
|
||||
@@ -86,9 +86,9 @@ func newDsyncNodes(endpoints EndpointList) (clnts []dsync.NetLocker, myNode int)
|
||||
locker = &(receiver.ll)
|
||||
} else {
|
||||
host, err := xnet.ParseHost(endpoint.Host)
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to parse Lock RPC Host", context.Background())
|
||||
locker, err = NewLockRPCClient(host)
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to initialize Lock RPC Client", context.Background())
|
||||
}
|
||||
|
||||
clnts = append(clnts, locker)
|
||||
|
||||
@@ -226,7 +226,7 @@ func TestNamespaceForceUnlockTest(t *testing.T) {
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
// Signalled so all is fine.
|
||||
// Signaled so all is fine.
|
||||
break
|
||||
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
|
||||
+29
-65
@@ -68,28 +68,21 @@ type NotificationPeerErr struct {
|
||||
}
|
||||
|
||||
// DeleteBucket - calls DeleteBucket RPC call on all peers.
|
||||
func (sys *NotificationSys) DeleteBucket(bucketName string) <-chan NotificationPeerErr {
|
||||
errCh := make(chan NotificationPeerErr)
|
||||
func (sys *NotificationSys) DeleteBucket(ctx context.Context, bucketName string) {
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
if err := client.DeleteBucket(bucketName); err != nil {
|
||||
errCh <- NotificationPeerErr{
|
||||
Host: addr,
|
||||
Err: err,
|
||||
}
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}(addr, client)
|
||||
}
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
return errCh
|
||||
}
|
||||
|
||||
// SetCredentials - calls SetCredentials RPC call on all peers.
|
||||
@@ -120,104 +113,76 @@ func (sys *NotificationSys) SetCredentials(credentials auth.Credentials) map[xne
|
||||
}
|
||||
|
||||
// SetBucketPolicy - calls SetBucketPolicy RPC call on all peers.
|
||||
func (sys *NotificationSys) SetBucketPolicy(bucketName string, bucketPolicy *policy.Policy) <-chan NotificationPeerErr {
|
||||
errCh := make(chan NotificationPeerErr)
|
||||
func (sys *NotificationSys) SetBucketPolicy(ctx context.Context, bucketName string, bucketPolicy *policy.Policy) {
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
if err := client.SetBucketPolicy(bucketName, bucketPolicy); err != nil {
|
||||
errCh <- NotificationPeerErr{
|
||||
Host: addr,
|
||||
Err: err,
|
||||
}
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}(addr, client)
|
||||
}
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
return errCh
|
||||
}
|
||||
|
||||
// RemoveBucketPolicy - calls RemoveBucketPolicy RPC call on all peers.
|
||||
func (sys *NotificationSys) RemoveBucketPolicy(bucketName string) <-chan NotificationPeerErr {
|
||||
errCh := make(chan NotificationPeerErr)
|
||||
func (sys *NotificationSys) RemoveBucketPolicy(ctx context.Context, bucketName string) {
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
if err := client.RemoveBucketPolicy(bucketName); err != nil {
|
||||
errCh <- NotificationPeerErr{
|
||||
Host: addr,
|
||||
Err: err,
|
||||
}
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}(addr, client)
|
||||
}
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
return errCh
|
||||
}
|
||||
|
||||
// PutBucketNotification - calls PutBucketNotification RPC call on all peers.
|
||||
func (sys *NotificationSys) PutBucketNotification(bucketName string, rulesMap event.RulesMap) <-chan NotificationPeerErr {
|
||||
errCh := make(chan NotificationPeerErr)
|
||||
func (sys *NotificationSys) PutBucketNotification(ctx context.Context, bucketName string, rulesMap event.RulesMap) {
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient, rulesMap event.RulesMap) {
|
||||
defer wg.Done()
|
||||
if err := client.PutBucketNotification(bucketName, rulesMap); err != nil {
|
||||
errCh <- NotificationPeerErr{
|
||||
Host: addr,
|
||||
Err: err,
|
||||
}
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}(addr, client, rulesMap.Clone())
|
||||
}
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
return errCh
|
||||
}
|
||||
|
||||
// ListenBucketNotification - calls ListenBucketNotification RPC call on all peers.
|
||||
func (sys *NotificationSys) ListenBucketNotification(bucketName string, eventNames []event.Name, pattern string,
|
||||
targetID event.TargetID, localPeer xnet.Host) <-chan NotificationPeerErr {
|
||||
errCh := make(chan NotificationPeerErr)
|
||||
func (sys *NotificationSys) ListenBucketNotification(ctx context.Context, bucketName string, eventNames []event.Name, pattern string,
|
||||
targetID event.TargetID, localPeer xnet.Host) {
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for addr, client := range sys.peerRPCClientMap {
|
||||
wg.Add(1)
|
||||
go func(addr xnet.Host, client *PeerRPCClient) {
|
||||
defer wg.Done()
|
||||
if err := client.ListenBucketNotification(bucketName, eventNames, pattern, targetID, localPeer); err != nil {
|
||||
errCh <- NotificationPeerErr{
|
||||
Host: addr,
|
||||
Err: err,
|
||||
}
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", addr.Name)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}(addr, client)
|
||||
}
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
return errCh
|
||||
}
|
||||
|
||||
// AddRemoteTarget - adds event rules map, HTTP/PeerRPC client target to bucket name.
|
||||
@@ -464,12 +429,8 @@ func (sys *NotificationSys) Send(args eventArgs) []event.TargetIDErr {
|
||||
}
|
||||
|
||||
// NewNotificationSys - creates new notification system object.
|
||||
func NewNotificationSys(config *serverConfig, endpoints EndpointList) (*NotificationSys, error) {
|
||||
targetList, err := getNotificationTargets(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func NewNotificationSys(config *serverConfig, endpoints EndpointList) *NotificationSys {
|
||||
targetList := getNotificationTargets(config)
|
||||
peerRPCClientMap := makeRemoteRPCClients(endpoints)
|
||||
|
||||
// bucketRulesMap/bucketRemoteTargetRulesMap are initialized by NotificationSys.Init()
|
||||
@@ -478,7 +439,7 @@ func NewNotificationSys(config *serverConfig, endpoints EndpointList) (*Notifica
|
||||
bucketRulesMap: make(map[string]event.RulesMap),
|
||||
bucketRemoteTargetRulesMap: make(map[string]map[event.TargetID]event.RulesMap),
|
||||
peerRPCClientMap: peerRPCClientMap,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type eventArgs struct {
|
||||
@@ -556,13 +517,16 @@ func sendEvent(args eventArgs) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, err := range globalNotificationSys.Send(args) {
|
||||
reqInfo := &logger.ReqInfo{BucketName: args.BucketName, ObjectName: args.Object.Name}
|
||||
reqInfo.AppendTags("EventName", args.EventName.String())
|
||||
reqInfo.AppendTags("targetID", err.ID.Name)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogOnceIf(ctx, err.Err, err.ID)
|
||||
}
|
||||
notifyCh := globalNotificationSys.Send(args)
|
||||
go func() {
|
||||
for _, err := range notifyCh {
|
||||
reqInfo := &logger.ReqInfo{BucketName: args.BucketName, ObjectName: args.Object.Name}
|
||||
reqInfo.AppendTags("EventName", args.EventName.String())
|
||||
reqInfo.AppendTags("targetID", err.ID.Name)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogOnceIf(ctx, err.Err, err.ID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func saveConfig(objAPI ObjectLayer, configFile string, data []byte) error {
|
||||
|
||||
@@ -320,13 +320,6 @@ func (e InvalidPart) Error() string {
|
||||
return "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag."
|
||||
}
|
||||
|
||||
// PartsSizeUnequal - All parts except the last part should be of the same size
|
||||
type PartsSizeUnequal struct{}
|
||||
|
||||
func (e PartsSizeUnequal) Error() string {
|
||||
return "All parts except the last part should be of the same size"
|
||||
}
|
||||
|
||||
// PartTooSmall - error if part size is less than 5MB.
|
||||
type PartTooSmall struct {
|
||||
PartSize int64
|
||||
|
||||
@@ -20,12 +20,15 @@ import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
)
|
||||
|
||||
@@ -277,6 +280,22 @@ func isMinioReservedBucket(bucketName string) bool {
|
||||
return bucketName == minioReservedBucket
|
||||
}
|
||||
|
||||
// returns a slice of hosts by reading a slice of DNS records
|
||||
func getHostsSlice(records []dns.SrvRecord) []string {
|
||||
var hosts []string
|
||||
for _, r := range records {
|
||||
hosts = append(hosts, r.Host)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
// returns a random host (and corresponding port) from a slice of DNS records
|
||||
func getRandomHostPort(records []dns.SrvRecord) (string, int) {
|
||||
rand.Seed(time.Now().Unix())
|
||||
srvRecord := records[rand.Intn(len(records))]
|
||||
return srvRecord.Host, srvRecord.Port
|
||||
}
|
||||
|
||||
// byBucketName is a collection satisfying sort.Interface.
|
||||
type byBucketName []BucketInfo
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
)
|
||||
|
||||
// Validates the preconditions for CopyObjectPart, returns true if CopyObjectPart
|
||||
@@ -243,7 +244,7 @@ func deleteObject(ctx context.Context, obj ObjectLayer, cache CacheObjectLayer,
|
||||
}
|
||||
|
||||
// Get host and port from Request.RemoteAddr.
|
||||
host, port, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
host, port, _ := net.SplitHostPort(handlers.GetSourceIP(r))
|
||||
|
||||
// Notify object deleted event.
|
||||
sendEvent(eventArgs{
|
||||
|
||||
+137
-50
@@ -32,8 +32,11 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
miniogo "github.com/minio/minio-go"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
@@ -65,7 +68,7 @@ func setHeadGetRespHeaders(w http.ResponseWriter, reqParams url.Values) {
|
||||
// This implementation of the GET operation retrieves object. To use GET,
|
||||
// you must have READ access to the object.
|
||||
func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "GetObject")
|
||||
ctx := newContext(r, w, "GetObject")
|
||||
|
||||
var object, bucket string
|
||||
vars := mux.Vars(r)
|
||||
@@ -157,7 +160,7 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
// additionally also skipping mod(offset)64KiB boundaries.
|
||||
writer = ioutil.LimitedWriter(writer, startOffset%(64*1024), length)
|
||||
|
||||
writer, startOffset, length, err = DecryptBlocksRequest(writer, r, startOffset, length, objInfo, false)
|
||||
writer, startOffset, length, err = DecryptBlocksRequest(writer, r, bucket, object, startOffset, length, objInfo, false)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -194,7 +197,7 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
// 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 {
|
||||
host, port = "", ""
|
||||
}
|
||||
@@ -215,7 +218,7 @@ func (api objectAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
// -----------
|
||||
// The HEAD operation retrieves metadata from an object without returning the object itself.
|
||||
func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "HeadObject")
|
||||
ctx := newContext(r, w, "HeadObject")
|
||||
|
||||
var object, bucket string
|
||||
vars := mux.Vars(r)
|
||||
@@ -266,7 +269,7 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeErrorResponse(w, apiErr, r.URL)
|
||||
return
|
||||
} else if encrypted {
|
||||
if _, err = DecryptRequest(w, r, objInfo.UserDefined); err != nil {
|
||||
if _, err = DecryptRequest(w, r, bucket, object, objInfo.UserDefined); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -290,7 +293,7 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// 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 {
|
||||
host, port = "", ""
|
||||
}
|
||||
@@ -309,7 +312,7 @@ func (api objectAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// Extract metadata relevant for an CopyObject operation based on conditional
|
||||
// header values specified in X-Amz-Metadata-Directive.
|
||||
func getCpObjMetadataFromHeader(ctx context.Context, header http.Header, userMeta map[string]string) (map[string]string, error) {
|
||||
func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta map[string]string) (map[string]string, error) {
|
||||
// Make a copy of the supplied metadata to avoid
|
||||
// to change the original one.
|
||||
defaultMeta := make(map[string]string, len(userMeta))
|
||||
@@ -319,13 +322,13 @@ func getCpObjMetadataFromHeader(ctx context.Context, header http.Header, userMet
|
||||
|
||||
// if x-amz-metadata-directive says REPLACE then
|
||||
// we extract metadata from the input headers.
|
||||
if isMetadataReplace(header) {
|
||||
return extractMetadataFromHeader(ctx, header)
|
||||
if isMetadataReplace(r.Header) {
|
||||
return extractMetadata(ctx, r)
|
||||
}
|
||||
|
||||
// if x-amz-metadata-directive says COPY then we
|
||||
// return the default metadata.
|
||||
if isMetadataCopy(header) {
|
||||
if isMetadataCopy(r.Header) {
|
||||
return defaultMeta, nil
|
||||
}
|
||||
|
||||
@@ -338,7 +341,7 @@ func getCpObjMetadataFromHeader(ctx context.Context, header http.Header, userMet
|
||||
// This implementation of the PUT operation adds an object to a bucket
|
||||
// while reading the object from another source.
|
||||
func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "CopyObject")
|
||||
ctx := newContext(r, w, "CopyObject")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
dstBucket := vars["bucket"]
|
||||
@@ -459,7 +462,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
for k, v := range srcInfo.UserDefined {
|
||||
encMetadata[k] = v
|
||||
}
|
||||
if err = rotateKey(oldKey, newKey, encMetadata); err != nil {
|
||||
if err = rotateKey(oldKey, newKey, srcBucket, srcObject, encMetadata); err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -471,7 +474,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
if sseCopyC {
|
||||
// Source is encrypted make sure to save the encrypted size.
|
||||
writer = ioutil.LimitedWriter(writer, 0, srcInfo.Size)
|
||||
writer, srcInfo.Size, err = DecryptAllBlocksCopyRequest(writer, r, srcInfo)
|
||||
writer, srcInfo.Size, err = DecryptAllBlocksCopyRequest(writer, r, srcBucket, srcObject, srcInfo)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
@@ -486,7 +489,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
}
|
||||
if sseC {
|
||||
reader, err = newEncryptReader(pipeReader, newKey, encMetadata)
|
||||
reader, err = newEncryptReader(reader, newKey, dstBucket, dstObject, encMetadata)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
@@ -510,7 +513,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
srcInfo.Writer = writer
|
||||
|
||||
srcInfo.UserDefined, err = getCpObjMetadataFromHeader(ctx, r.Header, srcInfo.UserDefined)
|
||||
srcInfo.UserDefined, err = getCpObjMetadataFromHeader(ctx, r, srcInfo.UserDefined)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
@@ -536,13 +539,73 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
// Copy source object to destination, if source and destination
|
||||
// object is same then only metadata is updated.
|
||||
objInfo, err := objectAPI.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
var objInfo ObjectInfo
|
||||
|
||||
// Checks if a remote putobject call is needed for CopyObject operation
|
||||
// 1. If source and destination bucket names are same, it means no call needed to etcd to get destination info
|
||||
// 2. If destination bucket doesn't exist locally, only then a etcd call is needed
|
||||
var isRemoteCallRequired = func(ctx context.Context, src, dst string, objAPI ObjectLayer) bool {
|
||||
if src == dst {
|
||||
return false
|
||||
}
|
||||
_, berr := objAPI.GetBucketInfo(ctx, dst)
|
||||
return berr == toObjectErr(errVolumeNotFound, dst)
|
||||
}
|
||||
|
||||
// Returns a minio-go Client configured to access remote host described by destDNSRecord
|
||||
// Applicable only in a federated deployment
|
||||
var getRemoteInstanceClient = func(host string, port int) (*miniogo.Core, error) {
|
||||
// In a federated deployment, all the instances share config files and hence expected to have same
|
||||
// credentials. So, access current instances creds and use it to create client for remote instance
|
||||
endpoint := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
accessKey := globalServerConfig.Credential.AccessKey
|
||||
secretKey := globalServerConfig.Credential.SecretKey
|
||||
return miniogo.NewCore(endpoint, accessKey, secretKey, globalIsSSL)
|
||||
}
|
||||
|
||||
if isRemoteCallRequired(ctx, srcBucket, dstBucket, objectAPI) {
|
||||
if globalDNSConfig == nil {
|
||||
writeErrorResponse(w, ErrNoSuchBucket, r.URL)
|
||||
return
|
||||
}
|
||||
var dstRecords []dns.SrvRecord
|
||||
if dstRecords, err = globalDNSConfig.Get(dstBucket); err == nil {
|
||||
go func() {
|
||||
if gerr := objectAPI.GetObject(ctx, srcBucket, srcObject, 0, srcInfo.Size, srcInfo.Writer, srcInfo.ETag); gerr != nil {
|
||||
pipeWriter.CloseWithError(gerr)
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
// Close writer explicitly to indicate data has been written
|
||||
srcInfo.Writer.Close()
|
||||
}()
|
||||
|
||||
// Send PutObject request to appropriate instance (in federated deployment)
|
||||
host, port := getRandomHostPort(dstRecords)
|
||||
client, rerr := getRemoteInstanceClient(host, port)
|
||||
if rerr != nil {
|
||||
pipeWriter.CloseWithError(rerr)
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
remoteObjInfo, rerr := client.PutObject(dstBucket, dstObject, srcInfo.Reader, srcInfo.Size, "", "", srcInfo.UserDefined)
|
||||
if rerr != nil {
|
||||
pipeWriter.CloseWithError(rerr)
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
objInfo.ETag = remoteObjInfo.ETag
|
||||
objInfo.ModTime = remoteObjInfo.LastModified
|
||||
}
|
||||
} else {
|
||||
// Copy source object to destination, if source and destination
|
||||
// object is same then only metadata is updated.
|
||||
objInfo, err = objectAPI.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pipeReader.Close()
|
||||
@@ -554,7 +617,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
|
||||
// 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 {
|
||||
host, port = "", ""
|
||||
}
|
||||
@@ -575,7 +638,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
// ----------
|
||||
// This implementation of the PUT operation adds an object to a bucket.
|
||||
func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "PutObject")
|
||||
ctx := newContext(r, w, "PutObject")
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
@@ -635,12 +698,12 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
// Extract metadata to be saved from incoming HTTP header.
|
||||
metadata, err := extractMetadataFromHeader(ctx, r.Header)
|
||||
metadata, err := extractMetadata(ctx, r)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if rAuthType == authTypeStreamingSigned {
|
||||
if contentEncoding, ok := metadata["content-encoding"]; ok {
|
||||
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
||||
@@ -725,7 +788,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
|
||||
if objectAPI.IsEncryptionSupported() {
|
||||
if hasSSECustomerHeader(r.Header) && !hasSuffix(object, slashSeparator) { // handle SSE-C requests
|
||||
reader, err = EncryptRequest(hashReader, r, metadata)
|
||||
reader, err = EncryptRequest(hashReader, r, bucket, object, metadata)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -742,6 +805,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
if api.CacheAPI() != nil && !hasSSECustomerHeader(r.Header) {
|
||||
putObject = api.CacheAPI().PutObject
|
||||
}
|
||||
|
||||
// Create the object..
|
||||
objInfo, err := putObject(ctx, bucket, object, hashReader, metadata)
|
||||
if err != nil {
|
||||
@@ -760,7 +824,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
|
||||
// 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 {
|
||||
host, port = "", ""
|
||||
}
|
||||
@@ -781,7 +845,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
|
||||
// NewMultipartUploadHandler - New multipart upload.
|
||||
func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "NewMultipartUpload")
|
||||
ctx := newContext(r, w, "NewMultipartUpload")
|
||||
|
||||
var object, bucket string
|
||||
vars := mux.Vars(r)
|
||||
@@ -824,7 +888,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
_, err = newEncryptMetadata(key, encMetadata)
|
||||
_, err = newEncryptMetadata(key, bucket, object, encMetadata)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -837,7 +901,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Extract metadata that needs to be saved.
|
||||
metadata, err := extractMetadataFromHeader(ctx, r.Header)
|
||||
metadata, err := extractMetadata(ctx, r)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, ErrInternalError, r.URL)
|
||||
return
|
||||
@@ -868,7 +932,7 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
|
||||
// CopyObjectPartHandler - uploads a part by copying data from an existing object as data source.
|
||||
func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "CopyObjectPart")
|
||||
ctx := newContext(r, w, "CopyObjectPart")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
dstBucket := vars["bucket"]
|
||||
@@ -973,6 +1037,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
|
||||
var writer io.WriteCloser = pipeWriter
|
||||
var reader io.Reader = pipeReader
|
||||
var getLength = length
|
||||
srcInfo.Reader, err = hash.NewReader(reader, length, "", "")
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
@@ -992,7 +1057,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
// Response writer should be limited early on for decryption upto required length,
|
||||
// additionally also skipping mod(offset)64KiB boundaries.
|
||||
writer = ioutil.LimitedWriter(writer, startOffset%(64*1024), length)
|
||||
writer, startOffset, length, err = DecryptBlocksRequest(pipeWriter, r, startOffset, length, srcInfo, true)
|
||||
writer, startOffset, getLength, err = DecryptBlocksRequest(writer, r, srcBucket, srcObject, startOffset, length, srcInfo, true)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
@@ -1012,28 +1077,29 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
// Calculating object encryption key
|
||||
var objectEncryptionKey []byte
|
||||
objectEncryptionKey, err = decryptObjectInfo(key, li.UserDefined)
|
||||
objectEncryptionKey, err = decryptObjectInfo(key, dstBucket, dstObject, li.UserDefined)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
reader, err = sio.EncryptReader(pipeReader, sio.Config{Key: objectEncryptionKey})
|
||||
var partIDbin [4]byte
|
||||
binary.LittleEndian.PutUint32(partIDbin[:], uint32(partID)) // marshal part ID
|
||||
|
||||
mac := hmac.New(sha256.New, objectEncryptionKey) // derive part encryption key from part ID and object key
|
||||
mac.Write(partIDbin[:])
|
||||
partEncryptionKey := mac.Sum(nil)
|
||||
reader, err = sio.EncryptReader(reader, sio.Config{Key: partEncryptionKey})
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
size := length
|
||||
if !sseCopyC {
|
||||
info := ObjectInfo{Size: length}
|
||||
size = info.EncryptedSize()
|
||||
}
|
||||
|
||||
info := ObjectInfo{Size: length}
|
||||
size := info.EncryptedSize()
|
||||
srcInfo.Reader, err = hash.NewReader(reader, size, "", "")
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
@@ -1047,7 +1113,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
// Copy source object to destination, if source and destination
|
||||
// object is same then only metadata is updated.
|
||||
partInfo, err := objectAPI.CopyObjectPart(ctx, srcBucket, srcObject, dstBucket,
|
||||
dstObject, uploadID, partID, startOffset, length, srcInfo)
|
||||
dstObject, uploadID, partID, startOffset, getLength, srcInfo)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -1065,7 +1131,7 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
|
||||
// PutObjectPartHandler - uploads an incoming part for an ongoing multipart operation.
|
||||
func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "PutObjectPart")
|
||||
ctx := newContext(r, w, "PutObjectPart")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -1217,7 +1283,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
|
||||
// Calculating object encryption key
|
||||
var objectEncryptionKey []byte
|
||||
objectEncryptionKey, err = decryptObjectInfo(key, li.UserDefined)
|
||||
objectEncryptionKey, err = decryptObjectInfo(key, bucket, object, li.UserDefined)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
@@ -1264,7 +1330,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
|
||||
// AbortMultipartUploadHandler - Abort multipart upload
|
||||
func (api objectAPIHandlers) AbortMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "AbortMultipartUpload")
|
||||
ctx := newContext(r, w, "AbortMultipartUpload")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -1303,7 +1369,7 @@ func (api objectAPIHandlers) AbortMultipartUploadHandler(w http.ResponseWriter,
|
||||
|
||||
// ListObjectPartsHandler - List object parts
|
||||
func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "ListObjectParts")
|
||||
ctx := newContext(r, w, "ListObjectParts")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -1343,7 +1409,7 @@ func (api objectAPIHandlers) ListObjectPartsHandler(w http.ResponseWriter, r *ht
|
||||
|
||||
// CompleteMultipartUploadHandler - Complete multipart upload.
|
||||
func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "CompleteMultipartUpload")
|
||||
ctx := newContext(r, w, "CompleteMultipartUpload")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -1431,7 +1497,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
writeSuccessResponseXML(w, encodedSuccessResponse)
|
||||
|
||||
// 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 {
|
||||
host, port = "", ""
|
||||
}
|
||||
@@ -1452,7 +1518,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
|
||||
// DeleteObjectHandler - delete an object
|
||||
func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, "DeleteObject")
|
||||
ctx := newContext(r, w, "DeleteObject")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
@@ -1477,6 +1543,27 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
if globalDNSConfig != nil {
|
||||
_, err := globalDNSConfig.Get(bucket)
|
||||
if err != nil {
|
||||
if err == dns.ErrNoEntriesFound {
|
||||
writeErrorResponse(w, ErrNoSuchBucket, r.URL)
|
||||
} else {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
getBucketInfo := objectAPI.GetBucketInfo
|
||||
if api.CacheAPI() != nil {
|
||||
getBucketInfo = api.CacheAPI().GetBucketInfo
|
||||
}
|
||||
if _, err := getBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectDELETE.html
|
||||
// Ignore delete object errors while replying to client, since we are
|
||||
// suppposed to reply only 204. Additionally log the error for
|
||||
|
||||
+14
-14
@@ -263,7 +263,7 @@ func testAPIGetObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrNoSuchKey), getGetObjectURL("", bucketName, "abcd"))),
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrNoSuchKey), getGetObjectURL("", bucketName, "abcd"), "")),
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
// Test case - 3.
|
||||
@@ -287,7 +287,7 @@ func testAPIGetObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
|
||||
accessKey: credentials.AccessKey,
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrInvalidRange), getGetObjectURL("", bucketName, objectName))),
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrInvalidRange), getGetObjectURL("", bucketName, objectName), "")),
|
||||
expectedRespStatus: http.StatusRequestedRangeNotSatisfiable,
|
||||
},
|
||||
// Test case - 5.
|
||||
@@ -313,7 +313,7 @@ func testAPIGetObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
|
||||
accessKey: "Invalid-AccessID",
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrInvalidAccessKeyID), getGetObjectURL("", bucketName, objectName))),
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrInvalidAccessKeyID), getGetObjectURL("", bucketName, objectName), "")),
|
||||
expectedRespStatus: http.StatusForbidden,
|
||||
},
|
||||
// Test case - 7.
|
||||
@@ -326,7 +326,7 @@ func testAPIGetObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrInvalidObjectName),
|
||||
getGetObjectURL("", bucketName, "../../etc"))),
|
||||
getGetObjectURL("", bucketName, "../../etc"), "")),
|
||||
expectedRespStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test case - 8.
|
||||
@@ -339,7 +339,7 @@ func testAPIGetObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrNoSuchKey),
|
||||
"/"+bucketName+"/"+". ./. ./etc")),
|
||||
"/"+bucketName+"/"+". ./. ./etc", "")),
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
// Test case - 9.
|
||||
@@ -352,7 +352,7 @@ func testAPIGetObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrInvalidObjectName),
|
||||
"/"+bucketName+"/"+". ./../etc")),
|
||||
"/"+bucketName+"/"+". ./../etc", "")),
|
||||
expectedRespStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test case - 10.
|
||||
@@ -365,7 +365,7 @@ func testAPIGetObjectHandler(obj ObjectLayer, instanceType, bucketName string, a
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrNoSuchKey),
|
||||
getGetObjectURL("", bucketName, "etc/path/proper/.../etc"))),
|
||||
getGetObjectURL("", bucketName, "etc/path/proper/.../etc"), "")),
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
@@ -2238,7 +2238,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(toAPIErrorCode(InvalidPart{})),
|
||||
getGetObjectURL("", bucketName, objectName))),
|
||||
getGetObjectURL("", bucketName, objectName), "")),
|
||||
expectedRespStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test case - 2.
|
||||
@@ -2253,7 +2253,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrMalformedXML),
|
||||
getGetObjectURL("", bucketName, objectName))),
|
||||
getGetObjectURL("", bucketName, objectName), "")),
|
||||
expectedRespStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test case - 3.
|
||||
@@ -2268,7 +2268,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(toAPIErrorCode(InvalidUploadID{UploadID: "abc"})),
|
||||
getGetObjectURL("", bucketName, objectName))),
|
||||
getGetObjectURL("", bucketName, objectName), "")),
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
},
|
||||
// Test case - 4.
|
||||
@@ -2283,7 +2283,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
|
||||
expectedContent: encodeResponse(completeMultipartAPIError{int64(4), int64(5242880), 1, "e2fc714c4727ee9395f324cd2e7f331f",
|
||||
getAPIErrorResponse(getAPIError(toAPIErrorCode(PartTooSmall{PartNumber: 1})),
|
||||
getGetObjectURL("", bucketName, objectName))}),
|
||||
getGetObjectURL("", bucketName, objectName), "")}),
|
||||
expectedRespStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test case - 5.
|
||||
@@ -2297,7 +2297,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(toAPIErrorCode(InvalidPart{})),
|
||||
getGetObjectURL("", bucketName, objectName))),
|
||||
getGetObjectURL("", bucketName, objectName), "")),
|
||||
expectedRespStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test case - 6.
|
||||
@@ -2312,7 +2312,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrInvalidPartOrder),
|
||||
getGetObjectURL("", bucketName, objectName))),
|
||||
getGetObjectURL("", bucketName, objectName), "")),
|
||||
expectedRespStatus: http.StatusBadRequest,
|
||||
},
|
||||
// Test case - 7.
|
||||
@@ -2327,7 +2327,7 @@ func testAPICompleteMultipartHandler(obj ObjectLayer, instanceType, bucketName s
|
||||
secretKey: credentials.SecretKey,
|
||||
|
||||
expectedContent: encodeResponse(getAPIErrorResponse(getAPIError(ErrInvalidAccessKeyID),
|
||||
getGetObjectURL("", bucketName, objectName))),
|
||||
getGetObjectURL("", bucketName, objectName), "")),
|
||||
expectedRespStatus: http.StatusForbidden,
|
||||
},
|
||||
// Test case - 8.
|
||||
|
||||
@@ -166,9 +166,9 @@ func makeRemoteRPCClients(endpoints EndpointList) map[xnet.Host]*PeerRPCClient {
|
||||
peerRPCClientMap := make(map[xnet.Host]*PeerRPCClient)
|
||||
for _, hostStr := range GetRemotePeers(endpoints) {
|
||||
host, err := xnet.ParseHost(hostStr)
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to parse peer RPC Host", context.Background())
|
||||
rpcClient, err := NewPeerRPCClient(host)
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to parse peer RPC Client", context.Background())
|
||||
peerRPCClientMap[*host] = rpcClient
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ func (receiver *peerRPCReceiver) SetCredentials(args *SetCredentialsArgs, reply
|
||||
prevCred := globalServerConfig.SetCredential(args.Credentials)
|
||||
|
||||
// Save credentials to config file
|
||||
if err := globalServerConfig.Save(); err != nil {
|
||||
if err := globalServerConfig.Save(getConfigFile()); err != nil {
|
||||
// As saving configurstion failed, restore previous credential in memory.
|
||||
globalServerConfig.SetCredential(prevCred)
|
||||
|
||||
@@ -201,7 +201,7 @@ func NewPeerRPCServer() (*xrpc.Server, error) {
|
||||
// registerPeerRPCRouter - creates and registers Peer RPC server and its router.
|
||||
func registerPeerRPCRouter(router *mux.Router) {
|
||||
rpcServer, err := NewPeerRPCServer()
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to initialize peer RPC Server", context.Background())
|
||||
subrouter := router.PathPrefix(minioReservedBucketPath).Subrouter()
|
||||
subrouter.Path(peerServiceSubPath).Handler(rpcServer)
|
||||
}
|
||||
|
||||
+3
-3
@@ -99,7 +99,7 @@ func (sys *PolicySys) refresh(objAPI ObjectLayer) error {
|
||||
}
|
||||
sys.removeDeletedBuckets(buckets)
|
||||
for _, bucket := range buckets {
|
||||
config, err := GetPolicyConfig(objAPI, bucket.Name)
|
||||
config, err := objAPI.GetBucketPolicy(context.Background(), bucket.Name)
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketPolicyNotFound); ok {
|
||||
sys.Remove(bucket.Name)
|
||||
@@ -187,8 +187,8 @@ func getConditionValues(request *http.Request, locationConstraint string) map[st
|
||||
return args
|
||||
}
|
||||
|
||||
// GetPolicyConfig - get policy config for given bucket name.
|
||||
func GetPolicyConfig(objAPI ObjectLayer, bucketName string) (*policy.Policy, error) {
|
||||
// getPolicyConfig - get policy config for given bucket name.
|
||||
func getPolicyConfig(objAPI ObjectLayer, bucketName string) (*policy.Policy, error) {
|
||||
// Construct path to policy.json for the given bucket.
|
||||
configFile := path.Join(bucketConfigPrefix, bucketName, bucketPolicyConfig)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user