mirror of
https://github.com/pgsty/minio.git
synced 2026-07-31 07:45:19 +03:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -89,7 +89,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:
|
||||
|
||||
@@ -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,16 @@ div.fesl-row {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(.fesl-row-selected) {
|
||||
background: lighten(@text-muted-color, 22%);
|
||||
.fis-icon {
|
||||
&:before {
|
||||
.opacity(0)
|
||||
}
|
||||
}
|
||||
|
||||
.fis-helper {
|
||||
&:before {
|
||||
.opacity(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,14 +137,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 +161,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 +210,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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -818,7 +818,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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+31
-21
@@ -17,10 +17,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
@@ -864,6 +866,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 +896,12 @@ 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 e, ok := err.(*client.Error); ok {
|
||||
if e.Code == client.ErrorCodeKeyNotFound {
|
||||
return ErrNoSuchBucket
|
||||
}
|
||||
}
|
||||
|
||||
switch err.(type) {
|
||||
|
||||
+5
-2
@@ -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()
|
||||
}
|
||||
|
||||
+118
-11
@@ -17,8 +17,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -28,13 +30,61 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/dns"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
)
|
||||
|
||||
// Check if there are buckets on server without corresponding entry in etcd backend and
|
||||
// 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 etcd.IsKeyNotFound(gerr) || 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.
|
||||
@@ -157,12 +207,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 && !etcd.IsKeyNotFound(err) && 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.
|
||||
@@ -211,7 +282,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 {
|
||||
@@ -353,12 +430,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 etcd.IsKeyNotFound(err) || 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)
|
||||
@@ -660,6 +758,15 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
|
||||
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.
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
@@ -224,11 +224,17 @@ 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())
|
||||
|
||||
@@ -241,10 +247,13 @@ 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)
|
||||
@@ -259,7 +268,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
|
||||
<-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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+56
-1
@@ -17,16 +17,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
|
||||
"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,12 +48,32 @@ 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 {
|
||||
kapi := etcd.NewKeysAPI(globalEtcdClient)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
_, err := kapi.Get(ctx, getConfigFile(), nil)
|
||||
cancel()
|
||||
if err == nil {
|
||||
logger.FatalIf(migrateConfig(), "Config migration failed.")
|
||||
logger.FatalIf(loadConfig(), "Unable to load config version: '%s'.", serverConfigVersion)
|
||||
} else {
|
||||
if etcd.IsKeyNotFound(err) {
|
||||
logger.FatalIf(newConfig(), "Unable to initialize minio config for the first time.")
|
||||
logger.Info("Created minio configuration file successfully at %v", globalEtcdClient.Endpoints())
|
||||
} else {
|
||||
logger.FatalIf(err, "Unable to load config version: '%s'.", serverConfigVersion)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
@@ -123,8 +150,36 @@ 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,
|
||||
Transport: NewCustomHTTPTransport(),
|
||||
})
|
||||
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 {
|
||||
|
||||
+38
-7
@@ -127,10 +127,19 @@ func (s *serverConfig) GetCacheConfig() CacheConfig {
|
||||
return s.Cache
|
||||
}
|
||||
|
||||
// Save config.
|
||||
func (s *serverConfig) Save() error {
|
||||
// Save config file.
|
||||
return quick.Save(getConfigFile(), s)
|
||||
// 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
|
||||
@@ -230,7 +239,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 {
|
||||
@@ -269,7 +281,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,7 +303,14 @@ 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
|
||||
}
|
||||
|
||||
configFile := getConfigFile()
|
||||
if err = srvCfg.Load(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+50
-49
@@ -21,6 +21,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
@@ -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
|
||||
}
|
||||
@@ -194,8 +195,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) || etcd.IsKeyNotFound(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("Unable to load config version ‘1’. %v", err)
|
||||
@@ -215,7 +216,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 +260,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 +275,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 +298,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 +313,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 +339,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 +354,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 +428,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 +443,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 +484,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 +499,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 +547,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 +561,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 +617,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 +631,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 +685,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 +699,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 +756,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 +770,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 +854,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 +867,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 +934,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 +947,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 +1019,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 +1032,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 +1108,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 +1122,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 +1198,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 +1212,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 +1319,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 +1333,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 +1423,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 +1435,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 +1529,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 +1541,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 +1634,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 +1646,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 +1738,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 +1750,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 +1842,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 +1854,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 +1955,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 +1967,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 +2068,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 +2080,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,7 +2186,7 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,6 @@ func fsStatVolume(ctx context.Context, volume string) (os.FileInfo, error) {
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
logger.LogIf(ctx, errVolumeAccessDenied)
|
||||
return nil, errVolumeAccessDenied
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
logger.Init(GOPATH)
|
||||
logger.Init(GOPATH, GOROOT)
|
||||
logger.RegisterUIError(fmtError)
|
||||
}
|
||||
|
||||
|
||||
@@ -238,6 +238,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 +294,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -97,12 +97,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 +119,46 @@ 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
|
||||
// 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"
|
||||
}
|
||||
|
||||
// Validate host parameters.
|
||||
if g.host != "" {
|
||||
// Override default params if the host is provided
|
||||
endpoint, secure, err = minio.ParseGatewayEndpoint(g.host)
|
||||
// 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
|
||||
}
|
||||
|
||||
if _, err = clnt.BucketExists("probe-bucket-sign"); err != nil {
|
||||
clnt, err = miniogo.NewV2(endpoint, accessKey, secretKey, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = clnt.BucketExists("probe-bucket-sign"); 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
|
||||
}
|
||||
|
||||
@@ -255,7 +271,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)
|
||||
|
||||
+74
-1
@@ -19,13 +19,19 @@ package cmd
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
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,6 +617,73 @@ 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 etcd.IsKeyNotFound(err) || 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.
|
||||
@@ -618,7 +691,7 @@ func (h pathValidityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// cancelled 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)
|
||||
|
||||
+18
-3
@@ -22,11 +22,15 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
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,15 +179,19 @@ 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
|
||||
|
||||
@@ -192,12 +201,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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
+41
-9
@@ -52,6 +52,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 ..
|
||||
}
|
||||
|
||||
@@ -142,22 +161,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 +188,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 +232,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+69
-7
@@ -32,7 +32,9 @@ 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/hash"
|
||||
"github.com/minio/minio/pkg/ioutil"
|
||||
@@ -536,13 +538,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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+2
-2
@@ -135,10 +135,10 @@ func getValidPath(path string) (string, error) {
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
defer os.Remove(pathJoin(path, ".writable-check.tmp"))
|
||||
file.Close()
|
||||
|
||||
err = os.Remove(pathJoin(path, ".writable-check.tmp"))
|
||||
return path, err
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// isDirEmpty - returns whether given directory is empty or not.
|
||||
|
||||
@@ -49,6 +49,8 @@ func registerDistXLRouters(router *mux.Router, endpoints EndpointList) {
|
||||
var globalHandlers = []HandlerFunc{
|
||||
// set HTTP security headers such as Content-Security-Policy.
|
||||
addSecurityHeaders,
|
||||
// Forward path style requests to actual host in a bucket federated setup.
|
||||
setBucketForwardingHandler,
|
||||
// Ratelimit the incoming requests using a token bucket algorithm
|
||||
setRateLimitHandler,
|
||||
// Validate all the incoming paths.
|
||||
|
||||
+11
-1
@@ -33,7 +33,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
logger.Init(GOPATH)
|
||||
logger.Init(GOPATH, GOROOT)
|
||||
logger.RegisterUIError(fmtError)
|
||||
}
|
||||
|
||||
@@ -89,6 +89,11 @@ ENVIRONMENT VARIABLES:
|
||||
WORM:
|
||||
MINIO_WORM: To turn on Write-Once-Read-Many in server, set this value to "on".
|
||||
|
||||
BUCKET-DNS:
|
||||
MINIO_DOMAIN: To enable bucket DNS requests, set this value to Minio host domain name.
|
||||
MINIO_PUBLIC_IPS: To enable bucket DNS requests, set this value to list of Minio host public IP(s) delimited by ",".
|
||||
MINIO_ETCD_ENDPOINTS: To enable bucket DNS requests, set this value to list of etcd endpoints delimited by ",".
|
||||
|
||||
EXAMPLES:
|
||||
1. Start minio server on "/home/shared" directory.
|
||||
$ {{.HelpName}} /home/shared
|
||||
@@ -303,6 +308,11 @@ func serverMain(ctx *cli.Context) {
|
||||
globalObjectAPI = newObject
|
||||
globalObjLayerMutex.Unlock()
|
||||
|
||||
// Populate existing buckets to the etcd backend
|
||||
if globalDNSConfig != nil {
|
||||
initFederatorBackend(newObject)
|
||||
}
|
||||
|
||||
// Prints the formatted startup message once object layer is initialized.
|
||||
apiEndpoints := getAPIEndpoints(globalMinioAddr)
|
||||
printStartupMessage(apiEndpoints)
|
||||
|
||||
@@ -39,6 +39,9 @@ func skipContentSha256Cksum(r *http.Request) bool {
|
||||
|
||||
if isRequestPresignedSignatureV4(r) {
|
||||
v, ok = r.URL.Query()["X-Amz-Content-Sha256"]
|
||||
if !ok {
|
||||
v, ok = r.Header["X-Amz-Content-Sha256"]
|
||||
}
|
||||
} else {
|
||||
v, ok = r.Header["X-Amz-Content-Sha256"]
|
||||
}
|
||||
@@ -62,6 +65,9 @@ func getContentSha256Cksum(r *http.Request) string {
|
||||
// will default to 'UNSIGNED-PAYLOAD'.
|
||||
defaultSha256Cksum = unsignedPayload
|
||||
v, ok = r.URL.Query()["X-Amz-Content-Sha256"]
|
||||
if !ok {
|
||||
v, ok = r.Header["X-Amz-Content-Sha256"]
|
||||
}
|
||||
} else {
|
||||
// X-Amz-Content-Sha256, if not set in signed requests, checksum
|
||||
// will default to sha256([]byte("")).
|
||||
|
||||
@@ -316,9 +316,9 @@ func NewStorageRPCClient(host *xnet.Host, endpointPath string) (*StorageRPCClien
|
||||
// Initialize new storage rpc client.
|
||||
func newStorageRPC(endpoint Endpoint) *StorageRPCClient {
|
||||
host, err := xnet.ParseHost(endpoint.Host)
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to parse storage RPC Host", context.Background())
|
||||
rpcClient, err := NewStorageRPCClient(host, endpoint.Path)
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
logger.FatalIf(err, "Unable to initialize storage RPC client", context.Background())
|
||||
rpcClient.connected = rpcClient.Call(storageServiceName+".Connect", &AuthArgs{}, &VoidReply{}) == nil
|
||||
return rpcClient
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@ func newTestConfig(bucketLocation string) (rootPath string, err error) {
|
||||
globalServerConfig.SetRegion(bucketLocation)
|
||||
|
||||
// Save config.
|
||||
if err = globalServerConfig.Save(); err != nil {
|
||||
if err = globalServerConfig.Save(getConfigFile()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
@@ -67,3 +67,6 @@ var errNotFirstDisk = errors.New("Not first disk")
|
||||
|
||||
// error returned by first disk waiting to initialize other servers.
|
||||
var errFirstDiskWait = errors.New("Waiting on other disks")
|
||||
|
||||
// error returned when a bucket already exists
|
||||
var errBucketAlreadyExists = errors.New("Your previous request to create the named bucket succeeded and you already own it")
|
||||
|
||||
+1
-1
@@ -258,7 +258,7 @@ func ToS3ETag(etag string) string {
|
||||
// used while communicating with the cloud backends.
|
||||
// This sets the value for MaxIdleConnsPerHost from 2 (go default)
|
||||
// to 100.
|
||||
func NewCustomHTTPTransport() http.RoundTripper {
|
||||
func NewCustomHTTPTransport() *http.Transport {
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
|
||||
+61
-18
@@ -31,6 +31,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/rpc/v2/json2"
|
||||
@@ -39,6 +40,7 @@ import (
|
||||
"github.com/minio/minio/browser"
|
||||
"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/hash"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
@@ -135,6 +137,26 @@ func (web *webAPIHandlers) MakeBucket(r *http.Request, args *MakeBucketArgs, rep
|
||||
return toJSONError(errInvalidBucketName)
|
||||
}
|
||||
|
||||
if globalDNSConfig != nil {
|
||||
if _, err := globalDNSConfig.Get(args.BucketName); err != nil {
|
||||
if etcd.IsKeyNotFound(err) || err == dns.ErrNoEntriesFound {
|
||||
// Proceed to creating a bucket.
|
||||
if err = objectAPI.MakeBucketWithLocation(context.Background(), args.BucketName, globalServerConfig.GetRegion()); err != nil {
|
||||
return toJSONError(err)
|
||||
}
|
||||
if err = globalDNSConfig.Put(args.BucketName); err != nil {
|
||||
objectAPI.DeleteBucket(context.Background(), args.BucketName)
|
||||
return toJSONError(err)
|
||||
}
|
||||
|
||||
reply.UIVersion = browser.UIVersion
|
||||
return nil
|
||||
}
|
||||
return toJSONError(err)
|
||||
}
|
||||
return toJSONError(errBucketAlreadyExists)
|
||||
}
|
||||
|
||||
if err := objectAPI.MakeBucketWithLocation(context.Background(), args.BucketName, globalServerConfig.GetRegion()); err != nil {
|
||||
return toJSONError(err, args.BucketName)
|
||||
}
|
||||
@@ -176,6 +198,14 @@ func (web *webAPIHandlers) DeleteBucket(r *http.Request, args *RemoveBucketArgs,
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
|
||||
if globalDNSConfig != nil {
|
||||
if err := globalDNSConfig.Delete(args.BucketName); err != nil {
|
||||
// Deleting DNS entry failed, attempt to create the bucket again.
|
||||
objectAPI.MakeBucketWithLocation(ctx, args.BucketName, "")
|
||||
return toJSONError(err)
|
||||
}
|
||||
}
|
||||
|
||||
reply.UIVersion = browser.UIVersion
|
||||
return nil
|
||||
}
|
||||
@@ -208,16 +238,32 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re
|
||||
if authErr != nil {
|
||||
return toJSONError(authErr)
|
||||
}
|
||||
buckets, err := listBuckets(context.Background())
|
||||
if err != nil {
|
||||
return toJSONError(err)
|
||||
}
|
||||
for _, bucket := range buckets {
|
||||
reply.Buckets = append(reply.Buckets, WebBucketInfo{
|
||||
Name: bucket.Name,
|
||||
CreationDate: bucket.Created,
|
||||
})
|
||||
// If etcd, dns federation configured list buckets from etcd.
|
||||
if globalDNSConfig != nil {
|
||||
dnsBuckets, err := globalDNSConfig.List()
|
||||
if err != nil {
|
||||
return toJSONError(err)
|
||||
}
|
||||
for _, dnsRecord := range dnsBuckets {
|
||||
bucketName := strings.Trim(dnsRecord.Key, "/")
|
||||
reply.Buckets = append(reply.Buckets, WebBucketInfo{
|
||||
Name: bucketName,
|
||||
CreationDate: dnsRecord.CreationDate,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
buckets, err := listBuckets(context.Background())
|
||||
if err != nil {
|
||||
return toJSONError(err)
|
||||
}
|
||||
for _, bucket := range buckets {
|
||||
reply.Buckets = append(reply.Buckets, WebBucketInfo{
|
||||
Name: bucket.Name,
|
||||
CreationDate: bucket.Created,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reply.UIVersion = browser.UIVersion
|
||||
return nil
|
||||
}
|
||||
@@ -412,11 +458,6 @@ type LoginRep struct {
|
||||
func (web *webAPIHandlers) Login(r *http.Request, args *LoginArgs, reply *LoginRep) error {
|
||||
token, err := authenticateWeb(args.Username, args.Password)
|
||||
if err != nil {
|
||||
// Make sure to log errors related to browser login,
|
||||
// for security and auditing reasons.
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("remoteAddr", r.RemoteAddr)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
return toJSONError(err)
|
||||
}
|
||||
|
||||
@@ -437,7 +478,9 @@ func (web webAPIHandlers) GenerateAuth(r *http.Request, args *WebGenericArgs, re
|
||||
return toJSONError(errAuthentication)
|
||||
}
|
||||
cred, err := auth.GetNewCredentials()
|
||||
logger.CriticalIf(context.Background(), err)
|
||||
if err != nil {
|
||||
return toJSONError(err)
|
||||
}
|
||||
reply.AccessKey = cred.AccessKey
|
||||
reply.SecretKey = cred.SecretKey
|
||||
reply.UIVersion = browser.UIVersion
|
||||
@@ -480,9 +523,9 @@ func (web *webAPIHandlers) SetAuth(r *http.Request, args *SetAuthArgs, reply *Se
|
||||
// Update credentials in memory
|
||||
prevCred := globalServerConfig.SetCredential(creds)
|
||||
|
||||
// Save credentials to config file
|
||||
if err := globalServerConfig.Save(); err != nil {
|
||||
// As saving configurstion failed, restore previous credential in memory.
|
||||
// Persist updated credentials.
|
||||
if err = globalServerConfig.Save(getConfigFile()); err != nil {
|
||||
// Save the current creds when failed to update.
|
||||
globalServerConfig.SetCredential(prevCred)
|
||||
logger.LogIf(context.Background(), err)
|
||||
return toJSONError(err)
|
||||
|
||||
+11
-2
@@ -375,8 +375,17 @@ func (xl xlObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID
|
||||
}
|
||||
|
||||
// Fetch buffer for I/O, returns from the pool if not allocates a new one and returns.
|
||||
buffer := xl.bp.Get()
|
||||
defer xl.bp.Put(buffer)
|
||||
var buffer []byte
|
||||
switch size := data.Size(); {
|
||||
case size == 0:
|
||||
buffer = make([]byte, 1) // Allocate atleast a byte to reach EOF
|
||||
case size < blockSizeV1:
|
||||
// No need to allocate fully blockSizeV1 buffer if the incoming data is smaller.
|
||||
buffer = make([]byte, size, 2*size)
|
||||
default:
|
||||
buffer = xl.bp.Get()
|
||||
defer xl.bp.Put(buffer)
|
||||
}
|
||||
|
||||
file, err := storage.CreateFile(ctx, data, minioMetaTmpBucket, tmpPartPath, buffer, DefaultBitrotAlgorithm, writeQuorum)
|
||||
if err != nil {
|
||||
|
||||
+11
-2
@@ -611,8 +611,17 @@ func (xl xlObjects) putObject(ctx context.Context, bucket string, object string,
|
||||
}
|
||||
|
||||
// Fetch buffer for I/O, returns from the pool if not allocates a new one and returns.
|
||||
buffer := xl.bp.Get()
|
||||
defer xl.bp.Put(buffer)
|
||||
var buffer []byte
|
||||
switch size := data.Size(); {
|
||||
case size == 0:
|
||||
buffer = make([]byte, 1) // Allocate atleast a byte to reach EOF
|
||||
case size < blockSizeV1:
|
||||
// No need to allocate fully blockSizeV1 buffer if the incoming data is smaller.
|
||||
buffer = make([]byte, size, 2*size)
|
||||
default:
|
||||
buffer = xl.bp.Get()
|
||||
defer xl.bp.Put(buffer)
|
||||
}
|
||||
|
||||
// Read data and split into parts - similar to multipart mechanism
|
||||
for partIdx := 1; ; partIdx++ {
|
||||
|
||||
@@ -8,7 +8,7 @@ Disk caching feature here refers to the use of caching disks to store content cl
|
||||
## Get started
|
||||
|
||||
### 1. Prerequisites
|
||||
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio).
|
||||
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide).
|
||||
|
||||
### 2. Run Minio with cache
|
||||
Disk caching can be enabled by updating the `cache` config settings for Minio server. Config `cache` settings takes the mounted drive(s) or directory paths, cache expiry duration (in days) and any wildcard patterns to exclude from being cached.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
. {
|
||||
etcd churchofminio.com {
|
||||
endpoint http://localhost:2379 http://localhost:4001
|
||||
upstream /etc/resolv.conf
|
||||
}
|
||||
debug
|
||||
prometheus
|
||||
cache 160 mydomain.com
|
||||
loadbalance
|
||||
proxy . /etc/resolv.conf
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# Federation Quickstart Guide [](https://slack.minio.io)
|
||||
This document explains how to configure Minio with `Bucket lookup from DNS` style federation.
|
||||
|
||||
## Get started
|
||||
|
||||
### 1. Prerequisites
|
||||
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide).
|
||||
|
||||
### 2. Run Minio in federated mode
|
||||
Bucket lookup from DNS federation requires two dependencies
|
||||
|
||||
- etcd (for config, bucket SRV records)
|
||||
- coredns (for DNS management based on populated bucket SRV records, optional)
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
### Environment variables
|
||||
|
||||
#### MINIO_ETCD_ENDPOINTS
|
||||
|
||||
This is comma separated list of etcd servers that you want to use as the Minio federation back-end. This should
|
||||
be same across the federated deployment, i.e. all the Minio instances within a federated deployment should use same
|
||||
etcd back-end.
|
||||
|
||||
#### MINIO_DOMAIN
|
||||
|
||||
This is the top level domain name used for the federated setup. This domain name should ideally resolve to a load-balancer
|
||||
running in front of all the federated Minio instances. The domain name is used to create sub domain entries to etcd. For
|
||||
example, if the domain is set to `domain.com`, the buckets `bucket1`, `bucket2` will be accessible as `bucket1.domain.com`
|
||||
and `bucket2.domain.com`.
|
||||
|
||||
#### MINIO_PUBLIC_IPS
|
||||
|
||||
This is comma separated list of IP addresses to which buckets created on this Minio instance will resolve to. For example,
|
||||
a bucket `bucket1` created on current Minio instance will be accessible as `bucket1.domain.com`, and the DNS entry for
|
||||
`bucket1.domain.com` will point to IP address set in `MINIO_PUBLIC_IPS`.
|
||||
|
||||
*Note*
|
||||
|
||||
- This field is mandatory for standalone and erasure code Minio server deployments, to enable federated mode.
|
||||
- This field is optional for distributed deployments. If you don't set this field in a federated setup, we use the IP addresses of
|
||||
hosts passed to the Minio server startup and use them for DNS entries.
|
||||
|
||||
### Run Multiple Clusters
|
||||
|
||||
> cluster1
|
||||
|
||||
```sh
|
||||
export MINIO_ETCD_ENDPOINTS="http://remote-etcd1:2379,http://remote-etcd2:4001"
|
||||
export MINIO_DOMAIN=domain.com
|
||||
export MINIO_PUBLIC_IPS=44.35.2.1,44.35.2.2,44.35.2.3,44.35.2.4
|
||||
minio server http://rack{1...4}.host{1...4}.domain.com/mnt/export{1...32}
|
||||
```
|
||||
|
||||
> cluster2
|
||||
|
||||
```sh
|
||||
export MINIO_ETCD_ENDPOINTS="http://remote-etcd1:2379,http://remote-etcd2:4001"
|
||||
export MINIO_DOMAIN=domain.com
|
||||
export MINIO_PUBLIC_IPS=44.35.1.1,44.35.1.2,44.35.1.3,44.35.1.4
|
||||
minio server http://rack{5...8}.host{5...8}.domain.com/mnt/export{1...32}
|
||||
```
|
||||
|
||||
In this configuration you can see `MINIO_ETCD_ENDPOINTS` points to the etcd backend which manages Minio's
|
||||
`config.json` and bucket DNS SRV records. `MINIO_DOMAIN` indicates the domain suffix for the bucket which
|
||||
will be used to resolve bucket through DNS. For example if you have a bucket such as `mybucket`, the
|
||||
client can use now `mybucket.domain.com` to directly resolve itself to the right cluster. `MINIO_PUBLIC_IPS`
|
||||
points to the public IP address where each cluster might be accessible, this is unique for each cluster.
|
||||
|
||||
NOTE: `mybucket` only exists on one cluster either `cluster1` or `cluster2` this is random and
|
||||
is decided by how `domain.com` gets resolved, if there is a round-robin DNS on `domain.com` then
|
||||
it is randomized which cluster might provision the bucket.
|
||||
|
||||
### 3. Test your setup
|
||||
To test this setup, access the Minio server via browser or [`mc`](https://docs.minio.io/docs/minio-client-quickstart-guide). You’ll see the uploaded files are accessible from the all the Minio endpoints.
|
||||
|
||||
# Explore Further
|
||||
- [Use `mc` with Minio Server](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||
- [Use `aws-cli` with Minio Server](https://docs.minio.io/docs/aws-cli-with-minio)
|
||||
- [Use `s3cmd` with Minio Server](https://docs.minio.io/docs/s3cmd-with-minio)
|
||||
- [Use `minio-go` SDK with Minio Server](https://docs.minio.io/docs/golang-client-quickstart-guide)
|
||||
- [The Minio documentation website](https://docs.minio.io)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -8,7 +8,7 @@ We call it large-bucket as it allows a single Minio bucket to expand over multip
|
||||
If you're aware of distributed Minio setup, the installation and configuration remains the same. Only new addition to the input syntax is `...` convention to abbreviate the drive arguments. Remote drives in a distributed setup are encoded as HTTP(s) URIs which can be similarly abbreviated as well.
|
||||
|
||||
### 1. Prerequisites
|
||||
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio).
|
||||
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide).
|
||||
|
||||
### 2. Run Minio on many drives
|
||||
We'll see examples on how to do this in the following sections.
|
||||
|
||||
@@ -5,7 +5,7 @@ version: '2'
|
||||
# 9001 through 9004.
|
||||
services:
|
||||
minio1:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- data1:/data
|
||||
ports:
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
MINIO_SECRET_KEY: minio123
|
||||
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
|
||||
minio2:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- data2:/data
|
||||
ports:
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
MINIO_SECRET_KEY: minio123
|
||||
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
|
||||
minio3:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- data3:/data
|
||||
ports:
|
||||
@@ -35,7 +35,7 @@ services:
|
||||
MINIO_SECRET_KEY: minio123
|
||||
command: server http://minio1/data http://minio2/data http://minio3/data http://minio4/data
|
||||
minio4:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- data4:/data
|
||||
ports:
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '3.1'
|
||||
|
||||
services:
|
||||
minio1:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- minio1-data:/export
|
||||
ports:
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
- access_key
|
||||
|
||||
minio2:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- minio2-data:/export
|
||||
ports:
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
- access_key
|
||||
|
||||
minio3:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- minio3-data:/export
|
||||
ports:
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
- access_key
|
||||
|
||||
minio4:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- minio4-data:/export
|
||||
ports:
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
minio1:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- minio1-data:/export
|
||||
ports:
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
|
||||
|
||||
minio2:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- minio2-data:/export
|
||||
ports:
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
|
||||
|
||||
minio3:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- minio3-data:/export
|
||||
ports:
|
||||
@@ -56,7 +56,7 @@ services:
|
||||
command: server http://minio1/export http://minio2/export http://minio3/export http://minio4/export
|
||||
|
||||
minio4:
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
volumes:
|
||||
- minio4-data:/export
|
||||
ports:
|
||||
|
||||
@@ -124,7 +124,7 @@ spec:
|
||||
- name: data
|
||||
mountPath: "/data"
|
||||
# Pulls the lastest Minio image from Docker Hub
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
args:
|
||||
- server
|
||||
- /data
|
||||
@@ -303,7 +303,7 @@ spec:
|
||||
value: "minio"
|
||||
- name: MINIO_SECRET_KEY
|
||||
value: "minio123"
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
args:
|
||||
- server
|
||||
- http://minio-0.minio.default.svc.cluster.local/data
|
||||
@@ -514,7 +514,7 @@ spec:
|
||||
containers:
|
||||
- name: minio
|
||||
# Pulls the default Minio image from Docker Hub
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
args:
|
||||
- gateway
|
||||
- gcs
|
||||
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
value: "minio"
|
||||
- name: MINIO_SECRET_KEY
|
||||
value: "minio123"
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
args:
|
||||
- server
|
||||
- http://minio-0.minio.default.svc.cluster.local/data
|
||||
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
containers:
|
||||
- name: minio
|
||||
# Pulls the default Minio image from Docker Hub
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
args:
|
||||
- gateway
|
||||
- gcs
|
||||
|
||||
@@ -29,7 +29,7 @@ spec:
|
||||
- name: data
|
||||
mountPath: "/data"
|
||||
# Pulls the lastest Minio image from Docker Hub
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
args:
|
||||
- server
|
||||
- /data
|
||||
|
||||
@@ -30,7 +30,7 @@ Above command deploys Minio on the Kubernetes cluster in the default configurati
|
||||
| Parameter | Description | Default |
|
||||
|----------------------------|-------------------------------------|---------------------------------------------------------|
|
||||
| `image` | Minio image name | `minio/minio` |
|
||||
| `imageTag` | Minio image tag. Possible values listed [here](https://hub.docker.com/r/minio/minio/tags/).| `RELEASE.2018-05-25T19-49-13Z`|
|
||||
| `imageTag` | Minio image tag. Possible values listed [here](https://hub.docker.com/r/minio/minio/tags/).| `RELEASE.2018-06-09T03-43-35Z`|
|
||||
| `imagePullPolicy` | Image pull policy | `Always` |
|
||||
| `mode` | Minio server mode (`standalone`, `shared` or `distributed`)| `standalone` |
|
||||
| `numberOfNodes` | Number of nodes (applicable only for Minio distributed mode). Should be 4 <= x <= 16 | `4` |
|
||||
|
||||
@@ -54,7 +54,7 @@ spec:
|
||||
value: "minio"
|
||||
- name: MINIO_SECRET_KEY
|
||||
value: "minio123"
|
||||
image: minio/minio:RELEASE.2018-05-25T19-49-13Z
|
||||
image: minio/minio:RELEASE.2018-06-09T03-43-35Z
|
||||
imagePullPolicy: IfNotPresent
|
||||
args: ["server", "http://minio-0.minio.default.svc.cluster.local/data", "http://minio-1.minio.default.svc.cluster.local/data", "http://minio-2.minio.default.svc.cluster.local/data", "http://minio-3.minio.default.svc.cluster.local/data"]
|
||||
ports:
|
||||
|
||||
@@ -18,7 +18,7 @@ If you're aware of stand-alone Minio set up, the installation and running remain
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio).
|
||||
Install Minio - [Minio Quickstart Guide](https://docs.minio.io/docs/minio-quickstart-guide).
|
||||
|
||||
## 2. Run Minio on Shared Backend
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
## 开始
|
||||
|
||||
### 1. 前期条件
|
||||
安装Minio - [Minio快速入门](https://docs.minio.io/docs/minio)。
|
||||
安装Minio - [Minio快速入门](https://docs.minio.io/docs/minio-quickstart-guide)。
|
||||
|
||||
### 2. 运行Minio缓存
|
||||
磁盘缓存可以通过修改Minio服务的`cache`配置来进行开启。配置`cache`设置需要指定磁盘路径、缓存过期时间(以天为单位)以及使用统配符方式指定的不需要进行缓存的对象。
|
||||
|
||||
@@ -8,7 +8,7 @@ Minio的纠删码功能限制了最多只能使用16块磁盘。这就限制了
|
||||
安装和部署方式和分布式Minio一样。只不过是在输入参数的语法上,用`...`来做为磁盘参数的简写。分布式设置中的远程磁盘被编码为HTTP(s)URI,它也可以被同样的缩写。
|
||||
|
||||
### 1. 前提条件
|
||||
安装Minio - [Minio快速入门](https://docs.minio.io/docs/minio)。
|
||||
安装Minio - [Minio快速入门](https://docs.minio.io/docs/minio-quickstart-guide)。
|
||||
|
||||
### 2. 在多个磁盘上运行Minio
|
||||
我们将在下面的章节中看到如何做到这一点的例子。
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ Minio is an object storage server released under Apache License v2.0.
|
||||
It is compatible with Amazon S3 cloud storage service. It is best
|
||||
suited for storing unstructured data such as photos, videos, log
|
||||
files, backups and container / VM images. Size of an object can
|
||||
range from a few KBs to a maximum of 5TB.
|
||||
range from a few KBs to a maximum of 5TiB.
|
||||
|
||||
%prep
|
||||
%setup -qc
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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 dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/pkg/set"
|
||||
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
)
|
||||
|
||||
// ErrNoEntriesFound - Indicates no entries were found for the given key (directory)
|
||||
var ErrNoEntriesFound = errors.New("No entries found for this key")
|
||||
|
||||
// create a new coredns service record for the bucket.
|
||||
func newCoreDNSMsg(bucket, ip string, port int, ttl uint32) ([]byte, error) {
|
||||
return json.Marshal(&SrvRecord{
|
||||
Host: ip,
|
||||
Port: port,
|
||||
TTL: ttl,
|
||||
CreationDate: time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
// Retrieves list of DNS entries for the domain.
|
||||
func (c *coreDNS) List() ([]SrvRecord, error) {
|
||||
key := msg.Path(fmt.Sprintf("%s.", c.domainName), defaultPrefixPath)
|
||||
return c.list(key)
|
||||
}
|
||||
|
||||
// Retrieves DNS records for a bucket.
|
||||
func (c *coreDNS) Get(bucket string) ([]SrvRecord, error) {
|
||||
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, c.domainName), defaultPrefixPath)
|
||||
return c.list(key)
|
||||
}
|
||||
|
||||
// Retrieves list of entries under the key passed.
|
||||
// Note that this method fetches entries upto only two levels deep.
|
||||
func (c *coreDNS) list(key string) ([]SrvRecord, error) {
|
||||
kapi := etcd.NewKeysAPI(c.etcdClient)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||
r, err := kapi.Get(ctx, key, &etcd.GetOptions{Recursive: true})
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var srvRecords []SrvRecord
|
||||
for _, n := range r.Node.Nodes {
|
||||
if !n.Dir {
|
||||
var srvRecord SrvRecord
|
||||
if err = json.Unmarshal([]byte(n.Value), &srvRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srvRecord.Key = strings.TrimPrefix(n.Key, key)
|
||||
srvRecords = append(srvRecords, srvRecord)
|
||||
} else {
|
||||
// As this is a directory, loop through all the nodes inside (assuming all nodes are non-directories)
|
||||
for _, n1 := range n.Nodes {
|
||||
var srvRecord SrvRecord
|
||||
if err = json.Unmarshal([]byte(n1.Value), &srvRecord); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srvRecord.Key = strings.TrimPrefix(n1.Key, key)
|
||||
srvRecord.Key = strings.TrimSuffix(srvRecord.Key, srvRecord.Host)
|
||||
srvRecords = append(srvRecords, srvRecord)
|
||||
}
|
||||
}
|
||||
}
|
||||
if srvRecords != nil {
|
||||
sort.Slice(srvRecords, func(i int, j int) bool {
|
||||
return srvRecords[i].Key < srvRecords[j].Key
|
||||
})
|
||||
} else {
|
||||
return nil, ErrNoEntriesFound
|
||||
}
|
||||
return srvRecords, nil
|
||||
}
|
||||
|
||||
// Adds DNS entries into etcd endpoint in CoreDNS etcd message format.
|
||||
func (c *coreDNS) Put(bucket string) error {
|
||||
for ip := range c.domainIPs {
|
||||
bucketMsg, err := newCoreDNSMsg(bucket, ip, c.domainPort, defaultTTL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kapi := etcd.NewKeysAPI(c.etcdClient)
|
||||
key := msg.Path(fmt.Sprintf("%s.%s", bucket, c.domainName), defaultPrefixPath)
|
||||
key = key + "/" + ip
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||
_, err = kapi.Set(ctx, key, string(bucketMsg), nil)
|
||||
cancel()
|
||||
if err != nil {
|
||||
ctx, cancel = context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||
kapi.Delete(ctx, key, &etcd.DeleteOptions{Recursive: true})
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Removes DNS entries added in Put().
|
||||
func (c *coreDNS) Delete(bucket string) error {
|
||||
kapi := etcd.NewKeysAPI(c.etcdClient)
|
||||
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, c.domainName), defaultPrefixPath)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||
_, err := kapi.Delete(ctx, key, &etcd.DeleteOptions{Recursive: true})
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
// CoreDNS - represents dns config for coredns server.
|
||||
type coreDNS struct {
|
||||
domainName string
|
||||
domainIPs set.StringSet
|
||||
domainPort int
|
||||
etcdClient etcd.Client
|
||||
}
|
||||
|
||||
// NewCoreDNS - initialize a new coreDNS set/unset values.
|
||||
func NewCoreDNS(domainName string, domainIPs set.StringSet, domainPort string, etcdClient etcd.Client) (Config, error) {
|
||||
if domainName == "" || domainIPs.IsEmpty() || etcdClient == nil {
|
||||
return nil, errors.New("invalid argument")
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(domainPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &coreDNS{
|
||||
domainName: domainName,
|
||||
domainIPs: domainIPs,
|
||||
domainPort: port,
|
||||
etcdClient: etcdClient,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 dns
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTTL = 30
|
||||
defaultPrefixPath = "/skydns"
|
||||
defaultContextTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
// SrvRecord - represents a DNS service record
|
||||
type SrvRecord struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Mail bool `json:"mail,omitempty"` // Be an MX record. Priority becomes Preference.
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
|
||||
// Holds info about when the entry was created first.
|
||||
CreationDate time.Time `json:"creationDate"`
|
||||
|
||||
// When a SRV record with a "Host: IP-address" is added, we synthesize
|
||||
// a srv.Target domain name. Normally we convert the full Key where
|
||||
// the record lives to a DNS name and use this as the srv.Target. When
|
||||
// TargetStrip > 0 we strip the left most TargetStrip labels from the
|
||||
// DNS name.
|
||||
TargetStrip int `json:"targetstrip,omitempty"`
|
||||
|
||||
// Group is used to group (or *not* to group) different services
|
||||
// together. Services with an identical Group are returned in
|
||||
// the same answer.
|
||||
Group string `json:"group,omitempty"`
|
||||
|
||||
// Key carries the original key used during Put().
|
||||
Key string `json:"-"`
|
||||
}
|
||||
|
||||
// Config - represents dns put, get interface. This interface can be
|
||||
// used to implement various backends as needed.
|
||||
type Config interface {
|
||||
Put(key string) error
|
||||
List() ([]SrvRecord, error)
|
||||
Get(key string) ([]SrvRecord, error)
|
||||
Delete(key string) error
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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 handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultFlushInterval = time.Duration(100) * time.Millisecond
|
||||
|
||||
// Forwarder forwards all incoming HTTP requests to configured transport.
|
||||
type Forwarder struct {
|
||||
RoundTripper http.RoundTripper
|
||||
PassHost bool
|
||||
|
||||
// internal variables
|
||||
rewriter *headerRewriter
|
||||
}
|
||||
|
||||
// NewForwarder creates an instance of Forwarder based on the provided list of configuration options
|
||||
func NewForwarder(f *Forwarder) *Forwarder {
|
||||
f.rewriter = &headerRewriter{}
|
||||
if f.RoundTripper == nil {
|
||||
f.RoundTripper = http.DefaultTransport
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// ServeHTTP forwards HTTP traffic using the configured transport
|
||||
func (f *Forwarder) ServeHTTP(w http.ResponseWriter, inReq *http.Request) {
|
||||
outReq := new(http.Request)
|
||||
*outReq = *inReq // includes shallow copies of maps, but we handle this in Director
|
||||
|
||||
revproxy := httputil.ReverseProxy{
|
||||
Director: func(req *http.Request) {
|
||||
f.modifyRequest(req, inReq.URL)
|
||||
},
|
||||
Transport: f.RoundTripper,
|
||||
FlushInterval: defaultFlushInterval,
|
||||
}
|
||||
revproxy.ServeHTTP(w, outReq)
|
||||
}
|
||||
|
||||
func (f *Forwarder) getURLFromRequest(req *http.Request) *url.URL {
|
||||
// If the Request was created by Go via a real HTTP request, RequestURI will
|
||||
// contain the original query string. If the Request was created in code, RequestURI
|
||||
// will be empty, and we will use the URL object instead
|
||||
u := req.URL
|
||||
if req.RequestURI != "" {
|
||||
parsedURL, err := url.ParseRequestURI(req.RequestURI)
|
||||
if err == nil {
|
||||
u = parsedURL
|
||||
}
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// copyURL provides update safe copy by avoiding shallow copying User field
|
||||
func copyURL(i *url.URL) *url.URL {
|
||||
out := *i
|
||||
if i.User != nil {
|
||||
out.User = &(*i.User)
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
// Modify the request to handle the target URL
|
||||
func (f *Forwarder) modifyRequest(outReq *http.Request, target *url.URL) {
|
||||
outReq.URL = copyURL(outReq.URL)
|
||||
outReq.URL.Scheme = target.Scheme
|
||||
outReq.URL.Host = target.Host
|
||||
|
||||
u := f.getURLFromRequest(outReq)
|
||||
|
||||
outReq.URL.Path = u.Path
|
||||
outReq.URL.RawPath = u.RawPath
|
||||
outReq.URL.RawQuery = u.RawQuery
|
||||
outReq.RequestURI = "" // Outgoing request should not have RequestURI
|
||||
|
||||
// Do not pass client Host header unless requested.
|
||||
if !f.PassHost {
|
||||
outReq.Host = target.Host
|
||||
}
|
||||
|
||||
// TODO: only supports HTTP 1.1 for now.
|
||||
outReq.Proto = "HTTP/1.1"
|
||||
outReq.ProtoMajor = 1
|
||||
outReq.ProtoMinor = 1
|
||||
|
||||
f.rewriter.Rewrite(outReq)
|
||||
|
||||
// Disable closeNotify when method GET for http pipelining
|
||||
if outReq.Method == http.MethodGet {
|
||||
quietReq := outReq.WithContext(context.Background())
|
||||
*outReq = *quietReq
|
||||
}
|
||||
}
|
||||
|
||||
// headerRewriter is responsible for removing hop-by-hop headers and setting forwarding headers
|
||||
type headerRewriter struct{}
|
||||
|
||||
// Clean up IP in case if it is ipv6 address and it has {zone} information in it, like
|
||||
// "[fe80::d806:a55d:eb1b:49cc%vEthernet (vmxnet3 Ethernet Adapter - Virtual Switch)]:64692"
|
||||
func ipv6fix(clientIP string) string {
|
||||
return strings.Split(clientIP, "%")[0]
|
||||
}
|
||||
|
||||
func (rw *headerRewriter) Rewrite(req *http.Request) {
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
clientIP = ipv6fix(clientIP)
|
||||
if req.Header.Get(xRealIP) == "" {
|
||||
req.Header.Set(xRealIP, clientIP)
|
||||
}
|
||||
}
|
||||
|
||||
xfProto := req.Header.Get(xForwardedProto)
|
||||
if xfProto == "" {
|
||||
if req.TLS != nil {
|
||||
req.Header.Set(xForwardedProto, "https")
|
||||
} else {
|
||||
req.Header.Set(xForwardedProto, "http")
|
||||
}
|
||||
}
|
||||
|
||||
if xfPort := req.Header.Get(xForwardedPort); xfPort == "" {
|
||||
req.Header.Set(xForwardedPort, forwardedPort(req))
|
||||
}
|
||||
|
||||
if xfHost := req.Header.Get(xForwardedHost); xfHost == "" && req.Host != "" {
|
||||
req.Header.Set(xForwardedHost, req.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func forwardedPort(req *http.Request) string {
|
||||
if req == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if _, port, err := net.SplitHostPort(req.Host); err == nil && port != "" {
|
||||
return port
|
||||
}
|
||||
|
||||
if req.TLS != nil {
|
||||
return "443"
|
||||
}
|
||||
|
||||
return "80"
|
||||
}
|
||||
@@ -26,8 +26,11 @@ import (
|
||||
var (
|
||||
// De-facto standard header keys.
|
||||
xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
|
||||
xForwardedHost = http.CanonicalHeaderKey("X-Forwarded-Host")
|
||||
xForwardedPort = http.CanonicalHeaderKey("X-Forwarded-Port")
|
||||
xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto")
|
||||
xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme")
|
||||
xForwardedServer = http.CanonicalHeaderKey("X-Forwarded-Server")
|
||||
xRealIP = http.CanonicalHeaderKey("X-Real-IP")
|
||||
)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package quick
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -27,7 +28,9 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@@ -122,6 +125,58 @@ func saveFileConfig(filename string, v interface{}) error {
|
||||
|
||||
}
|
||||
|
||||
func saveFileConfigEtcd(filename string, clnt etcd.Client, v interface{}) error {
|
||||
// Fetch filename's extension
|
||||
ext := filepath.Ext(filename)
|
||||
// Marshal data
|
||||
dataBytes, err := toMarshaller(ext)(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
dataBytes = []byte(strings.Replace(string(dataBytes), "\n", "\r\n", -1))
|
||||
}
|
||||
|
||||
kapi := etcd.NewKeysAPI(clnt)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
_, err = kapi.Update(ctx, filename, string(dataBytes))
|
||||
if etcd.IsKeyNotFound(err) {
|
||||
_, err = kapi.Create(ctx, filename, string(dataBytes))
|
||||
}
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
func loadFileConfigEtcd(filename string, clnt etcd.Client, v interface{}) error {
|
||||
kapi := etcd.NewKeysAPI(clnt)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
resp, err := kapi.Get(ctx, filename, nil)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ev *etcd.Node
|
||||
switch {
|
||||
case resp.Node.Dir:
|
||||
for _, ev = range resp.Node.Nodes {
|
||||
if string(ev.Key) == filename {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
ev = resp.Node
|
||||
}
|
||||
|
||||
fileData := ev.Value
|
||||
if runtime.GOOS == "windows" {
|
||||
fileData = strings.Replace(ev.Value, "\r\n", "\n", -1)
|
||||
}
|
||||
|
||||
// Unmarshal file's content
|
||||
return toUnmarshaller(filepath.Ext(filename))([]byte(fileData), v)
|
||||
}
|
||||
|
||||
// loadFileConfig unmarshals the file's content with the right
|
||||
// decoder format according to the filename extension. If no
|
||||
// extension is provided, json will be selected by default.
|
||||
|
||||
+48
-32
@@ -26,6 +26,7 @@ import (
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
"github.com/fatih/structs"
|
||||
"github.com/minio/minio/pkg/safe"
|
||||
)
|
||||
@@ -44,6 +45,7 @@ type Config interface {
|
||||
// config - implements quick.Config interface
|
||||
type config struct {
|
||||
data interface{}
|
||||
clnt etcd.Client
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -67,6 +69,10 @@ func (d config) Save(filename string) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if d.clnt != nil {
|
||||
return saveFileConfigEtcd(filename, d.clnt, d.data)
|
||||
}
|
||||
|
||||
// Backup if given file exists
|
||||
oldData, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
@@ -92,6 +98,9 @@ func (d config) Save(filename string) error {
|
||||
func (d config) Load(filename string) error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
if d.clnt != nil {
|
||||
return loadFileConfigEtcd(filename, d.clnt, d.data)
|
||||
}
|
||||
return loadFileConfig(filename, d.data)
|
||||
}
|
||||
|
||||
@@ -100,7 +109,7 @@ func (d config) Data() interface{} {
|
||||
return d.data
|
||||
}
|
||||
|
||||
//Diff - list fields that are in A but not in B
|
||||
// Diff - list fields that are in A but not in B
|
||||
func (d config) Diff(c Config) ([]structs.Field, error) {
|
||||
var fields []structs.Field
|
||||
|
||||
@@ -179,43 +188,50 @@ func writeFile(filename string, data []byte) error {
|
||||
return safeFile.Close()
|
||||
}
|
||||
|
||||
// New - instantiate a new config
|
||||
func New(data interface{}) (Config, error) {
|
||||
// GetVersion - extracts the version information.
|
||||
func GetVersion(filename string, clnt etcd.Client) (version string, err error) {
|
||||
var qc Config
|
||||
qc, err = LoadConfig(filename, clnt, &struct {
|
||||
Version string
|
||||
}{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return qc.Version(), nil
|
||||
}
|
||||
|
||||
// LoadConfig - loads json config from filename for the a given struct data
|
||||
func LoadConfig(filename string, clnt etcd.Client, data interface{}) (qc Config, err error) {
|
||||
qc, err = NewConfig(data, clnt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return qc, qc.Load(filename)
|
||||
}
|
||||
|
||||
// SaveConfig - saves given configuration data into given file as JSON.
|
||||
func SaveConfig(data interface{}, filename string, clnt etcd.Client) (err error) {
|
||||
if err = checkData(data); err != nil {
|
||||
return err
|
||||
}
|
||||
var qc Config
|
||||
qc, err = NewConfig(data, clnt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qc.Save(filename)
|
||||
}
|
||||
|
||||
// NewConfig loads config from etcd client if provided, otherwise loads from a local filename.
|
||||
// fails when all else fails.
|
||||
func NewConfig(data interface{}, clnt etcd.Client) (cfg Config, err error) {
|
||||
if err := checkData(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := new(config)
|
||||
d.data = data
|
||||
d.clnt = clnt
|
||||
d.lock = new(sync.RWMutex)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// GetVersion - extracts the version information.
|
||||
func GetVersion(filename string) (version string, err error) {
|
||||
var qc Config
|
||||
if qc, err = Load(filename, &struct {
|
||||
Version string
|
||||
}{}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return qc.Version(), err
|
||||
}
|
||||
|
||||
// Load - loads json config from filename for the a given struct data
|
||||
func Load(filename string, data interface{}) (qc Config, err error) {
|
||||
if qc, err = New(data); err == nil {
|
||||
err = qc.Load(filename)
|
||||
}
|
||||
return qc, err
|
||||
}
|
||||
|
||||
// Save - saves given configuration data into given file as JSON.
|
||||
func Save(filename string, data interface{}) (err error) {
|
||||
var qc Config
|
||||
if qc, err = New(data); err == nil {
|
||||
err = qc.Save(filename)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+26
-26
@@ -36,7 +36,7 @@ func TestReadVersion(t *testing.T) {
|
||||
Version string
|
||||
}
|
||||
saveMe := myStruct{"1"}
|
||||
config, err := New(&saveMe)
|
||||
config, err := NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func TestReadVersion(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
version, err := GetVersion("test.json")
|
||||
version, err := GetVersion("test.json", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func TestReadVersionErr(t *testing.T) {
|
||||
Version int
|
||||
}
|
||||
saveMe := myStruct{1}
|
||||
_, err := New(&saveMe)
|
||||
_, err := NewConfig(&saveMe, nil)
|
||||
if err == nil {
|
||||
t.Fatal("Unexpected should fail in initialization for bad input")
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func TestReadVersionErr(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = GetVersion("test.json")
|
||||
_, err = GetVersion("test.json", nil)
|
||||
if err == nil {
|
||||
t.Fatal("Unexpected should fail to fetch version")
|
||||
}
|
||||
@@ -79,7 +79,7 @@ func TestReadVersionErr(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = GetVersion("test.json")
|
||||
_, err = GetVersion("test.json", nil)
|
||||
if err == nil {
|
||||
t.Fatal("Unexpected should fail to fetch version")
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func TestSaveFailOnDir(t *testing.T) {
|
||||
Version string
|
||||
}
|
||||
saveMe := myStruct{"1"}
|
||||
config, err := New(&saveMe)
|
||||
config, err := NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -155,7 +155,7 @@ func TestLoadFile(t *testing.T) {
|
||||
Directories []string
|
||||
}
|
||||
saveMe := myStruct{}
|
||||
_, err := Load("test.json", &saveMe)
|
||||
_, err := LoadConfig("test.json", nil, &saveMe)
|
||||
if err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -167,11 +167,11 @@ func TestLoadFile(t *testing.T) {
|
||||
if err = file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = Load("test.json", &saveMe)
|
||||
_, err = LoadConfig("test.json", nil, &saveMe)
|
||||
if err == nil {
|
||||
t.Fatal("Unexpected should fail to load empty JSON")
|
||||
}
|
||||
config, err := New(&saveMe)
|
||||
config, err := NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -186,7 +186,7 @@ func TestLoadFile(t *testing.T) {
|
||||
}
|
||||
|
||||
saveMe = myStruct{"1", "guest", "nopassword", []string{"Work", "Documents", "Music"}}
|
||||
config, err = New(&saveMe)
|
||||
config, err = NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func TestLoadFile(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saveMe1 := myStruct{}
|
||||
_, err = Load("test.json", &saveMe1)
|
||||
_, err = LoadConfig("test.json", nil, &saveMe1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -240,7 +240,7 @@ directories:
|
||||
saveMe := myStruct{"1", "guest", "nopassword", []string{"Work", "Documents", "Music"}}
|
||||
|
||||
// Save format using
|
||||
config, err := New(&saveMe)
|
||||
config, err := NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -262,7 +262,7 @@ directories:
|
||||
|
||||
// Check if the loaded data is the same as the saved one
|
||||
loadMe := myStruct{}
|
||||
config, err = New(&loadMe)
|
||||
config, err = NewConfig(&loadMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -306,7 +306,7 @@ func TestJSONFormat(t *testing.T) {
|
||||
saveMe := myStruct{"1", "guest", "nopassword", []string{"Work", "Documents", "Music"}}
|
||||
|
||||
// Save format using
|
||||
config, err := New(&saveMe)
|
||||
config, err := NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -328,7 +328,7 @@ func TestJSONFormat(t *testing.T) {
|
||||
|
||||
// Check if the loaded data is the same as the saved one
|
||||
loadMe := myStruct{}
|
||||
config, err = New(&loadMe)
|
||||
config, err = NewConfig(&loadMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -351,7 +351,7 @@ func TestSaveLoad(t *testing.T) {
|
||||
Directories []string
|
||||
}
|
||||
saveMe := myStruct{"1", "guest", "nopassword", []string{"Work", "Documents", "Music"}}
|
||||
config, err := New(&saveMe)
|
||||
config, err := NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -361,7 +361,7 @@ func TestSaveLoad(t *testing.T) {
|
||||
}
|
||||
|
||||
loadMe := myStruct{Version: "1"}
|
||||
newConfig, err := New(&loadMe)
|
||||
newConfig, err := NewConfig(&loadMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -393,7 +393,7 @@ func TestSaveBackup(t *testing.T) {
|
||||
Directories []string
|
||||
}
|
||||
saveMe := myStruct{"1", "guest", "nopassword", []string{"Work", "Documents", "Music"}}
|
||||
config, err := New(&saveMe)
|
||||
config, err := NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -403,7 +403,7 @@ func TestSaveBackup(t *testing.T) {
|
||||
}
|
||||
|
||||
loadMe := myStruct{Version: "1"}
|
||||
newConfig, err := New(&loadMe)
|
||||
newConfig, err := NewConfig(&loadMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -424,7 +424,7 @@ func TestSaveBackup(t *testing.T) {
|
||||
t.Fatal("Expected to mismatch but succeeded instead")
|
||||
}
|
||||
|
||||
config, err = New(&mismatch)
|
||||
config, err = NewConfig(&mismatch, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -442,20 +442,20 @@ func TestDiff(t *testing.T) {
|
||||
Directories []string
|
||||
}
|
||||
saveMe := myStruct{"1", "guest", "nopassword", []string{"Work", "Documents", "Music"}}
|
||||
config, err := New(&saveMe)
|
||||
config, err := NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type myNewStruct struct {
|
||||
type myNewConfigStruct struct {
|
||||
Version string
|
||||
// User string
|
||||
Password string
|
||||
Directories []string
|
||||
}
|
||||
|
||||
mismatch := myNewStruct{"1", "nopassword", []string{"Work", "documents", "Music"}}
|
||||
newConfig, err := New(&mismatch)
|
||||
mismatch := myNewConfigStruct{"1", "nopassword", []string{"Work", "documents", "Music"}}
|
||||
newConfig, err := NewConfig(&mismatch, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -482,13 +482,13 @@ func TestDeepDiff(t *testing.T) {
|
||||
Directories []string
|
||||
}
|
||||
saveMe := myStruct{"1", "guest", "nopassword", []string{"Work", "Documents", "Music"}}
|
||||
config, err := New(&saveMe)
|
||||
config, err := NewConfig(&saveMe, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mismatch := myStruct{"1", "Guest", "nopassword", []string{"Work", "documents", "Music"}}
|
||||
newConfig, err := New(&mismatch)
|
||||
newConfig, err := NewConfig(&mismatch, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package msg
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Path converts a domainname to an etcd path. If s looks like service.staging.skydns.local.,
|
||||
// the resulting key will be /skydns/local/skydns/staging/service .
|
||||
func Path(s, prefix string) string {
|
||||
l := dns.SplitDomainName(s)
|
||||
for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
return path.Join(append([]string{"/" + prefix + "/"}, l...)...)
|
||||
}
|
||||
|
||||
// Domain is the opposite of Path.
|
||||
func Domain(s string) string {
|
||||
l := strings.Split(s, "/")
|
||||
// start with 1, to strip /skydns
|
||||
for i, j := 1, len(l)-1; i < j; i, j = i+1, j-1 {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
return dnsutil.Join(l[1 : len(l)-1])
|
||||
}
|
||||
|
||||
// PathWithWildcard ascts as Path, but if a name contains wildcards (* or any), the name will be
|
||||
// chopped of before the (first) wildcard, and we do a highler evel search and
|
||||
// later find the matching names. So service.*.skydns.local, will look for all
|
||||
// services under skydns.local and will later check for names that match
|
||||
// service.*.skydns.local. If a wildcard is found the returned bool is true.
|
||||
func PathWithWildcard(s, prefix string) (string, bool) {
|
||||
l := dns.SplitDomainName(s)
|
||||
for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 {
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
for i, k := range l {
|
||||
if k == "*" || k == "any" {
|
||||
return path.Join(append([]string{"/" + prefix + "/"}, l[:i]...)...), true
|
||||
}
|
||||
}
|
||||
return path.Join(append([]string{"/" + prefix + "/"}, l...)...), false
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
// Package msg defines the Service structure which is used for service discovery.
|
||||
package msg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Service defines a discoverable service in etcd. It is the rdata from a SRV
|
||||
// record, but with a twist. Host (Target in SRV) must be a domain name, but
|
||||
// if it looks like an IP address (4/6), we will treat it like an IP address.
|
||||
type Service struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Mail bool `json:"mail,omitempty"` // Be an MX record. Priority becomes Preference.
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
|
||||
// When a SRV record with a "Host: IP-address" is added, we synthesize
|
||||
// a srv.Target domain name. Normally we convert the full Key where
|
||||
// the record lives to a DNS name and use this as the srv.Target. When
|
||||
// TargetStrip > 0 we strip the left most TargetStrip labels from the
|
||||
// DNS name.
|
||||
TargetStrip int `json:"targetstrip,omitempty"`
|
||||
|
||||
// Group is used to group (or *not* to group) different services
|
||||
// together. Services with an identical Group are returned in the same
|
||||
// answer.
|
||||
Group string `json:"group,omitempty"`
|
||||
|
||||
// Etcd key where we found this service and ignored from json un-/marshalling
|
||||
Key string `json:"-"`
|
||||
}
|
||||
|
||||
// RR returns an RR representation of s. It is in a condensed form to minimize space
|
||||
// when this is returned in a DNS message.
|
||||
// The RR will look like:
|
||||
// 1.rails.production.east.skydns.local. 300 CH TXT "service1.example.com:8080(10,0,,false)[0,]"
|
||||
// etcd Key Ttl Host:Port < see below >
|
||||
// between parens: (Priority, Weight, Text (only first 200 bytes!), Mail)
|
||||
// between blockquotes: [TargetStrip,Group]
|
||||
// If the record is synthesised by CoreDNS (i.e. no lookup in etcd happened):
|
||||
//
|
||||
// TODO(miek): what to put here?
|
||||
//
|
||||
func (s *Service) RR() *dns.TXT {
|
||||
l := len(s.Text)
|
||||
if l > 200 {
|
||||
l = 200
|
||||
}
|
||||
t := new(dns.TXT)
|
||||
t.Hdr.Class = dns.ClassCHAOS
|
||||
t.Hdr.Ttl = s.TTL
|
||||
t.Hdr.Rrtype = dns.TypeTXT
|
||||
t.Hdr.Name = Domain(s.Key)
|
||||
|
||||
t.Txt = make([]string, 1)
|
||||
t.Txt[0] = fmt.Sprintf("%s:%d(%d,%d,%s,%t)[%d,%s]",
|
||||
s.Host, s.Port,
|
||||
s.Priority, s.Weight, s.Text[:l], s.Mail,
|
||||
s.TargetStrip, s.Group)
|
||||
return t
|
||||
}
|
||||
|
||||
// NewSRV returns a new SRV record based on the Service.
|
||||
func (s *Service) NewSRV(name string, weight uint16) *dns.SRV {
|
||||
host := targetStrip(dns.Fqdn(s.Host), s.TargetStrip)
|
||||
|
||||
return &dns.SRV{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: s.TTL},
|
||||
Priority: uint16(s.Priority), Weight: weight, Port: uint16(s.Port), Target: dns.Fqdn(host)}
|
||||
}
|
||||
|
||||
// NewMX returns a new MX record based on the Service.
|
||||
func (s *Service) NewMX(name string) *dns.MX {
|
||||
host := targetStrip(dns.Fqdn(s.Host), s.TargetStrip)
|
||||
|
||||
return &dns.MX{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: s.TTL},
|
||||
Preference: uint16(s.Priority), Mx: host}
|
||||
}
|
||||
|
||||
// NewA returns a new A record based on the Service.
|
||||
func (s *Service) NewA(name string, ip net.IP) *dns.A {
|
||||
return &dns.A{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: s.TTL}, A: ip}
|
||||
}
|
||||
|
||||
// NewAAAA returns a new AAAA record based on the Service.
|
||||
func (s *Service) NewAAAA(name string, ip net.IP) *dns.AAAA {
|
||||
return &dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: s.TTL}, AAAA: ip}
|
||||
}
|
||||
|
||||
// NewCNAME returns a new CNAME record based on the Service.
|
||||
func (s *Service) NewCNAME(name string, target string) *dns.CNAME {
|
||||
return &dns.CNAME{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: s.TTL}, Target: dns.Fqdn(target)}
|
||||
}
|
||||
|
||||
// NewTXT returns a new TXT record based on the Service.
|
||||
func (s *Service) NewTXT(name string) *dns.TXT {
|
||||
return &dns.TXT{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: s.TTL}, Txt: split255(s.Text)}
|
||||
}
|
||||
|
||||
// NewPTR returns a new PTR record based on the Service.
|
||||
func (s *Service) NewPTR(name string, target string) *dns.PTR {
|
||||
return &dns.PTR{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: s.TTL}, Ptr: dns.Fqdn(target)}
|
||||
}
|
||||
|
||||
// NewNS returns a new NS record based on the Service.
|
||||
func (s *Service) NewNS(name string) *dns.NS {
|
||||
host := targetStrip(dns.Fqdn(s.Host), s.TargetStrip)
|
||||
return &dns.NS{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: s.TTL}, Ns: host}
|
||||
}
|
||||
|
||||
// Group checks the services in sx, it looks for a Group attribute on the shortest
|
||||
// keys. If there are multiple shortest keys *and* the group attribute disagrees (and
|
||||
// is not empty), we don't consider it a group.
|
||||
// If a group is found, only services with *that* group (or no group) will be returned.
|
||||
func Group(sx []Service) []Service {
|
||||
if len(sx) == 0 {
|
||||
return sx
|
||||
}
|
||||
|
||||
// Shortest key with group attribute sets the group for this set.
|
||||
group := sx[0].Group
|
||||
slashes := strings.Count(sx[0].Key, "/")
|
||||
length := make([]int, len(sx))
|
||||
for i, s := range sx {
|
||||
x := strings.Count(s.Key, "/")
|
||||
length[i] = x
|
||||
if x < slashes {
|
||||
if s.Group == "" {
|
||||
break
|
||||
}
|
||||
slashes = x
|
||||
group = s.Group
|
||||
}
|
||||
}
|
||||
|
||||
if group == "" {
|
||||
return sx
|
||||
}
|
||||
|
||||
ret := []Service{} // with slice-tricks in sx we can prolly save this allocation (TODO)
|
||||
|
||||
for i, s := range sx {
|
||||
if s.Group == "" {
|
||||
ret = append(ret, s)
|
||||
continue
|
||||
}
|
||||
|
||||
// Disagreement on the same level
|
||||
if length[i] == slashes && s.Group != group {
|
||||
return sx
|
||||
}
|
||||
|
||||
if s.Group == group {
|
||||
ret = append(ret, s)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Split255 splits a string into 255 byte chunks.
|
||||
func split255(s string) []string {
|
||||
if len(s) < 255 {
|
||||
return []string{s}
|
||||
}
|
||||
sx := []string{}
|
||||
p, i := 0, 255
|
||||
for {
|
||||
if i <= len(s) {
|
||||
sx = append(sx, s[p:i])
|
||||
} else {
|
||||
sx = append(sx, s[p:])
|
||||
break
|
||||
|
||||
}
|
||||
p, i = p+255, i+255
|
||||
}
|
||||
|
||||
return sx
|
||||
}
|
||||
|
||||
// targetStrip strips "targetstrip" labels from the left side of the fully qualified name.
|
||||
func targetStrip(name string, targetStrip int) string {
|
||||
if targetStrip == 0 {
|
||||
return name
|
||||
}
|
||||
|
||||
offset, end := 0, false
|
||||
for i := 0; i < targetStrip; i++ {
|
||||
offset, end = dns.NextLabel(name, offset)
|
||||
}
|
||||
if end {
|
||||
// We overshot the name, use the orignal one.
|
||||
offset = 0
|
||||
}
|
||||
name = name[offset:]
|
||||
return name
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package msg
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// HostType returns the DNS type of what is encoded in the Service Host field. We're reusing
|
||||
// dns.TypeXXX to not reinvent a new set of identifiers.
|
||||
//
|
||||
// dns.TypeA: the service's Host field contains an A record.
|
||||
// dns.TypeAAAA: the service's Host field contains an AAAA record.
|
||||
// dns.TypeCNAME: the service's Host field contains a name.
|
||||
//
|
||||
// Note that a service can double/triple as a TXT record or MX record.
|
||||
func (s *Service) HostType() (what uint16, normalized net.IP) {
|
||||
|
||||
ip := net.ParseIP(s.Host)
|
||||
|
||||
switch {
|
||||
case ip == nil:
|
||||
return dns.TypeCNAME, nil
|
||||
|
||||
case ip.To4() != nil:
|
||||
return dns.TypeA, ip.To4()
|
||||
|
||||
case ip.To4() == nil:
|
||||
return dns.TypeAAAA, ip.To16()
|
||||
}
|
||||
// This should never be reached.
|
||||
return dns.TypeNone, nil
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package dnsutil
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// DuplicateCNAME returns true if r already exists in records.
|
||||
func DuplicateCNAME(r *dns.CNAME, records []dns.RR) bool {
|
||||
for _, rec := range records {
|
||||
if v, ok := rec.(*dns.CNAME); ok {
|
||||
if v.Target == r.Target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package dnsutil
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// Dedup de-duplicates a message.
|
||||
func Dedup(m *dns.Msg) *dns.Msg {
|
||||
// TODO(miek): expensive!
|
||||
m.Answer = dns.Dedup(m.Answer, nil)
|
||||
m.Ns = dns.Dedup(m.Ns, nil)
|
||||
m.Extra = dns.Dedup(m.Extra, nil)
|
||||
return m
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Package dnsutil contains DNS related helper functions.
|
||||
package dnsutil
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package dnsutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ParseHostPortOrFile parses the strings in s, each string can either be a address,
|
||||
// address:port or a filename. The address part is checked and the filename case a
|
||||
// resolv.conf like file is parsed and the nameserver found are returned.
|
||||
func ParseHostPortOrFile(s ...string) ([]string, error) {
|
||||
var servers []string
|
||||
for _, host := range s {
|
||||
addr, _, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
// Parse didn't work, it is not a addr:port combo
|
||||
if net.ParseIP(host) == nil {
|
||||
// Not an IP address.
|
||||
ss, err := tryFile(host)
|
||||
if err == nil {
|
||||
servers = append(servers, ss...)
|
||||
continue
|
||||
}
|
||||
return servers, fmt.Errorf("not an IP address or file: %q", host)
|
||||
}
|
||||
ss := net.JoinHostPort(host, "53")
|
||||
servers = append(servers, ss)
|
||||
continue
|
||||
}
|
||||
|
||||
if net.ParseIP(addr) == nil {
|
||||
// No an IP address.
|
||||
ss, err := tryFile(host)
|
||||
if err == nil {
|
||||
servers = append(servers, ss...)
|
||||
continue
|
||||
}
|
||||
return servers, fmt.Errorf("not an IP address or file: %q", host)
|
||||
}
|
||||
servers = append(servers, host)
|
||||
}
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// Try to open this is a file first.
|
||||
func tryFile(s string) ([]string, error) {
|
||||
c, err := dns.ClientConfigFromFile(s)
|
||||
if err == os.ErrNotExist {
|
||||
return nil, fmt.Errorf("failed to open file %q: %q", s, err)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
servers := []string{}
|
||||
for _, s := range c.Servers {
|
||||
servers = append(servers, net.JoinHostPort(s, c.Port))
|
||||
}
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// ParseHostPort will check if the host part is a valid IP address, if the
|
||||
// IP address is valid, but no port is found, defaultPort is added.
|
||||
func ParseHostPort(s, defaultPort string) (string, error) {
|
||||
addr, port, err := net.SplitHostPort(s)
|
||||
if port == "" {
|
||||
port = defaultPort
|
||||
}
|
||||
if err != nil {
|
||||
if net.ParseIP(s) == nil {
|
||||
return "", fmt.Errorf("must specify an IP address: `%s'", s)
|
||||
}
|
||||
return net.JoinHostPort(s, port), nil
|
||||
}
|
||||
|
||||
if net.ParseIP(addr) == nil {
|
||||
return "", fmt.Errorf("must specify an IP address: `%s'", addr)
|
||||
}
|
||||
return net.JoinHostPort(addr, port), nil
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package dnsutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Join joins labels to form a fully qualified domain name. If the last label is
|
||||
// the root label it is ignored. Not other syntax checks are performed.
|
||||
func Join(labels []string) string {
|
||||
ll := len(labels)
|
||||
if labels[ll-1] == "." {
|
||||
s := strings.Join(labels[:ll-1], ".")
|
||||
return dns.Fqdn(s)
|
||||
}
|
||||
s := strings.Join(labels, ".")
|
||||
return dns.Fqdn(s)
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package dnsutil
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExtractAddressFromReverse turns a standard PTR reverse record name
|
||||
// into an IP address. This works for ipv4 or ipv6.
|
||||
//
|
||||
// 54.119.58.176.in-addr.arpa. becomes 176.58.119.54. If the conversion
|
||||
// fails the empty string is returned.
|
||||
func ExtractAddressFromReverse(reverseName string) string {
|
||||
search := ""
|
||||
|
||||
f := reverse
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(reverseName, v4arpaSuffix):
|
||||
search = strings.TrimSuffix(reverseName, v4arpaSuffix)
|
||||
case strings.HasSuffix(reverseName, v6arpaSuffix):
|
||||
search = strings.TrimSuffix(reverseName, v6arpaSuffix)
|
||||
f = reverse6
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
// Reverse the segments and then combine them.
|
||||
return f(strings.Split(search, "."))
|
||||
}
|
||||
|
||||
func reverse(slice []string) string {
|
||||
for i := 0; i < len(slice)/2; i++ {
|
||||
j := len(slice) - i - 1
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
ip := net.ParseIP(strings.Join(slice, ".")).To4()
|
||||
if ip == nil {
|
||||
return ""
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
// reverse6 reverse the segments and combine them according to RFC3596:
|
||||
// b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2
|
||||
// is reversed to 2001:db8::567:89ab
|
||||
func reverse6(slice []string) string {
|
||||
for i := 0; i < len(slice)/2; i++ {
|
||||
j := len(slice) - i - 1
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
}
|
||||
slice6 := []string{}
|
||||
for i := 0; i < len(slice)/4; i++ {
|
||||
slice6 = append(slice6, strings.Join(slice[i*4:i*4+4], ""))
|
||||
}
|
||||
ip := net.ParseIP(strings.Join(slice6, ":")).To16()
|
||||
if ip == nil {
|
||||
return ""
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
const (
|
||||
// v4arpaSuffix is the reverse tree suffix for v4 IP addresses.
|
||||
v4arpaSuffix = ".in-addr.arpa."
|
||||
// v6arpaSuffix is the reverse tree suffix for v6 IP addresses.
|
||||
v6arpaSuffix = ".ip6.arpa."
|
||||
)
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package dnsutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// TrimZone removes the zone component from q. It returns the trimmed
|
||||
// name or an error is zone is longer then qname. The trimmed name will be returned
|
||||
// without a trailing dot.
|
||||
func TrimZone(q string, z string) (string, error) {
|
||||
zl := dns.CountLabel(z)
|
||||
i, ok := dns.PrevLabel(q, zl)
|
||||
if ok || i-1 < 0 {
|
||||
return "", errors.New("trimzone: overshot qname: " + q + "for zone " + z)
|
||||
}
|
||||
// This includes the '.', remove on return
|
||||
return q[:i-1], nil
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CoreOS Project
|
||||
Copyright 2014 CoreOS, Inc
|
||||
|
||||
This product includes software developed at CoreOS, Inc.
|
||||
(http://www.coreos.com/).
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
# etcd/client
|
||||
|
||||
etcd/client is the Go client library for etcd.
|
||||
|
||||
[](https://godoc.org/github.com/coreos/etcd/client)
|
||||
|
||||
etcd uses `cmd/vendor` directory to store external dependencies, which are
|
||||
to be compiled into etcd release binaries. `client` can be imported without
|
||||
vendoring. For full compatibility, it is recommended to vendor builds using
|
||||
etcd's vendored packages, using tools like godep, as in
|
||||
[vendor directories](https://golang.org/cmd/go/#hdr-Vendor_Directories).
|
||||
For more detail, please read [Go vendor design](https://golang.org/s/go15vendor).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go get github.com/coreos/etcd/client
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"github.com/coreos/etcd/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := client.Config{
|
||||
Endpoints: []string{"http://127.0.0.1:2379"},
|
||||
Transport: client.DefaultTransport,
|
||||
// set timeout per request to fail fast when the target endpoint is unavailable
|
||||
HeaderTimeoutPerRequest: time.Second,
|
||||
}
|
||||
c, err := client.New(cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
kapi := client.NewKeysAPI(c)
|
||||
// set "/foo" key with "bar" value
|
||||
log.Print("Setting '/foo' key with 'bar' value")
|
||||
resp, err := kapi.Set(context.Background(), "/foo", "bar", nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
// print common key info
|
||||
log.Printf("Set is done. Metadata is %q\n", resp)
|
||||
}
|
||||
// get "/foo" key's value
|
||||
log.Print("Getting '/foo' key value")
|
||||
resp, err = kapi.Get(context.Background(), "/foo", nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
// print common key info
|
||||
log.Printf("Get is done. Metadata is %q\n", resp)
|
||||
// print value
|
||||
log.Printf("%q key has %q value\n", resp.Node.Key, resp.Node.Value)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
etcd client might return three types of errors.
|
||||
|
||||
- context error
|
||||
|
||||
Each API call has its first parameter as `context`. A context can be canceled or have an attached deadline. If the context is canceled or reaches its deadline, the responding context error will be returned no matter what internal errors the API call has already encountered.
|
||||
|
||||
- cluster error
|
||||
|
||||
Each API call tries to send request to the cluster endpoints one by one until it successfully gets a response. If a requests to an endpoint fails, due to exceeding per request timeout or connection issues, the error will be added into a list of errors. If all possible endpoints fail, a cluster error that includes all encountered errors will be returned.
|
||||
|
||||
- response error
|
||||
|
||||
If the response gets from the cluster is invalid, a plain string error will be returned. For example, it might be a invalid JSON error.
|
||||
|
||||
Here is the example code to handle client errors:
|
||||
|
||||
```go
|
||||
cfg := client.Config{Endpoints: []string{"http://etcd1:2379","http://etcd2:2379","http://etcd3:2379"}}
|
||||
c, err := client.New(cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
kapi := client.NewKeysAPI(c)
|
||||
resp, err := kapi.Set(ctx, "test", "bar", nil)
|
||||
if err != nil {
|
||||
if err == context.Canceled {
|
||||
// ctx is canceled by another routine
|
||||
} else if err == context.DeadlineExceeded {
|
||||
// ctx is attached with a deadline and it exceeded
|
||||
} else if cerr, ok := err.(*client.ClusterError); ok {
|
||||
// process (cerr.Errors)
|
||||
} else {
|
||||
// bad cluster endpoints, which are not etcd servers
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Caveat
|
||||
|
||||
1. etcd/client prefers to use the same endpoint as long as the endpoint continues to work well. This saves socket resources, and improves efficiency for both client and server side. This preference doesn't remove consistency from the data consumed by the client because data replicated to each etcd member has already passed through the consensus process.
|
||||
|
||||
2. etcd/client does round-robin rotation on other available endpoints if the preferred endpoint isn't functioning properly. For example, if the member that etcd/client connects to is hard killed, etcd/client will fail on the first attempt with the killed member, and succeed on the second attempt with another member. If it fails to talk to all available endpoints, it will return all errors happened.
|
||||
|
||||
3. Default etcd/client cannot handle the case that the remote server is SIGSTOPed now. TCP keepalive mechanism doesn't help in this scenario because operating system may still send TCP keep-alive packets. Over time we'd like to improve this functionality, but solving this issue isn't high priority because a real-life case in which a server is stopped, but the connection is kept alive, hasn't been brought to our attention.
|
||||
|
||||
4. etcd/client cannot detect whether a member is healthy with watches and non-quorum read requests. If the member is isolated from the cluster, etcd/client may retrieve outdated data. Instead, users can either issue quorum read requests or monitor the /health endpoint for member health information.
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type Role struct {
|
||||
Role string `json:"role"`
|
||||
Permissions Permissions `json:"permissions"`
|
||||
Grant *Permissions `json:"grant,omitempty"`
|
||||
Revoke *Permissions `json:"revoke,omitempty"`
|
||||
}
|
||||
|
||||
type Permissions struct {
|
||||
KV rwPermission `json:"kv"`
|
||||
}
|
||||
|
||||
type rwPermission struct {
|
||||
Read []string `json:"read"`
|
||||
Write []string `json:"write"`
|
||||
}
|
||||
|
||||
type PermissionType int
|
||||
|
||||
const (
|
||||
ReadPermission PermissionType = iota
|
||||
WritePermission
|
||||
ReadWritePermission
|
||||
)
|
||||
|
||||
// NewAuthRoleAPI constructs a new AuthRoleAPI that uses HTTP to
|
||||
// interact with etcd's role creation and modification features.
|
||||
func NewAuthRoleAPI(c Client) AuthRoleAPI {
|
||||
return &httpAuthRoleAPI{
|
||||
client: c,
|
||||
}
|
||||
}
|
||||
|
||||
type AuthRoleAPI interface {
|
||||
// AddRole adds a role.
|
||||
AddRole(ctx context.Context, role string) error
|
||||
|
||||
// RemoveRole removes a role.
|
||||
RemoveRole(ctx context.Context, role string) error
|
||||
|
||||
// GetRole retrieves role details.
|
||||
GetRole(ctx context.Context, role string) (*Role, error)
|
||||
|
||||
// GrantRoleKV grants a role some permission prefixes for the KV store.
|
||||
GrantRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)
|
||||
|
||||
// RevokeRoleKV revokes some permission prefixes for a role on the KV store.
|
||||
RevokeRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)
|
||||
|
||||
// ListRoles lists roles.
|
||||
ListRoles(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
type httpAuthRoleAPI struct {
|
||||
client httpClient
|
||||
}
|
||||
|
||||
type authRoleAPIAction struct {
|
||||
verb string
|
||||
name string
|
||||
role *Role
|
||||
}
|
||||
|
||||
type authRoleAPIList struct{}
|
||||
|
||||
func (list *authRoleAPIList) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2AuthURL(ep, "roles", "")
|
||||
req, _ := http.NewRequest("GET", u.String(), nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func (l *authRoleAPIAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2AuthURL(ep, "roles", l.name)
|
||||
if l.role == nil {
|
||||
req, _ := http.NewRequest(l.verb, u.String(), nil)
|
||||
return req
|
||||
}
|
||||
b, err := json.Marshal(l.role)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
body := bytes.NewReader(b)
|
||||
req, _ := http.NewRequest(l.verb, u.String(), body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func (r *httpAuthRoleAPI) ListRoles(ctx context.Context) ([]string, error) {
|
||||
resp, body, err := r.client.Do(ctx, &authRoleAPIList{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var roleList struct {
|
||||
Roles []Role `json:"roles"`
|
||||
}
|
||||
if err = json.Unmarshal(body, &roleList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]string, 0, len(roleList.Roles))
|
||||
for _, r := range roleList.Roles {
|
||||
ret = append(ret, r.Role)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *httpAuthRoleAPI) AddRole(ctx context.Context, rolename string) error {
|
||||
role := &Role{
|
||||
Role: rolename,
|
||||
}
|
||||
return r.addRemoveRole(ctx, &authRoleAPIAction{
|
||||
verb: "PUT",
|
||||
name: rolename,
|
||||
role: role,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *httpAuthRoleAPI) RemoveRole(ctx context.Context, rolename string) error {
|
||||
return r.addRemoveRole(ctx, &authRoleAPIAction{
|
||||
verb: "DELETE",
|
||||
name: rolename,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *httpAuthRoleAPI) addRemoveRole(ctx context.Context, req *authRoleAPIAction) error {
|
||||
resp, body, err := r.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
|
||||
var sec authError
|
||||
err := json.Unmarshal(body, &sec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *httpAuthRoleAPI) GetRole(ctx context.Context, rolename string) (*Role, error) {
|
||||
return r.modRole(ctx, &authRoleAPIAction{
|
||||
verb: "GET",
|
||||
name: rolename,
|
||||
})
|
||||
}
|
||||
|
||||
func buildRWPermission(prefixes []string, permType PermissionType) rwPermission {
|
||||
var out rwPermission
|
||||
switch permType {
|
||||
case ReadPermission:
|
||||
out.Read = prefixes
|
||||
case WritePermission:
|
||||
out.Write = prefixes
|
||||
case ReadWritePermission:
|
||||
out.Read = prefixes
|
||||
out.Write = prefixes
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *httpAuthRoleAPI) GrantRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
|
||||
rwp := buildRWPermission(prefixes, permType)
|
||||
role := &Role{
|
||||
Role: rolename,
|
||||
Grant: &Permissions{
|
||||
KV: rwp,
|
||||
},
|
||||
}
|
||||
return r.modRole(ctx, &authRoleAPIAction{
|
||||
verb: "PUT",
|
||||
name: rolename,
|
||||
role: role,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *httpAuthRoleAPI) RevokeRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
|
||||
rwp := buildRWPermission(prefixes, permType)
|
||||
role := &Role{
|
||||
Role: rolename,
|
||||
Revoke: &Permissions{
|
||||
KV: rwp,
|
||||
},
|
||||
}
|
||||
return r.modRole(ctx, &authRoleAPIAction{
|
||||
verb: "PUT",
|
||||
name: rolename,
|
||||
role: role,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *httpAuthRoleAPI) modRole(ctx context.Context, req *authRoleAPIAction) (*Role, error) {
|
||||
resp, body, err := r.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
|
||||
var sec authError
|
||||
err = json.Unmarshal(body, &sec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, sec
|
||||
}
|
||||
var role Role
|
||||
if err = json.Unmarshal(body, &role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultV2AuthPrefix = "/v2/auth"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Roles []string `json:"roles"`
|
||||
Grant []string `json:"grant,omitempty"`
|
||||
Revoke []string `json:"revoke,omitempty"`
|
||||
}
|
||||
|
||||
// userListEntry is the user representation given by the server for ListUsers
|
||||
type userListEntry struct {
|
||||
User string `json:"user"`
|
||||
Roles []Role `json:"roles"`
|
||||
}
|
||||
|
||||
type UserRoles struct {
|
||||
User string `json:"user"`
|
||||
Roles []Role `json:"roles"`
|
||||
}
|
||||
|
||||
func v2AuthURL(ep url.URL, action string, name string) *url.URL {
|
||||
if name != "" {
|
||||
ep.Path = path.Join(ep.Path, defaultV2AuthPrefix, action, name)
|
||||
return &ep
|
||||
}
|
||||
ep.Path = path.Join(ep.Path, defaultV2AuthPrefix, action)
|
||||
return &ep
|
||||
}
|
||||
|
||||
// NewAuthAPI constructs a new AuthAPI that uses HTTP to
|
||||
// interact with etcd's general auth features.
|
||||
func NewAuthAPI(c Client) AuthAPI {
|
||||
return &httpAuthAPI{
|
||||
client: c,
|
||||
}
|
||||
}
|
||||
|
||||
type AuthAPI interface {
|
||||
// Enable auth.
|
||||
Enable(ctx context.Context) error
|
||||
|
||||
// Disable auth.
|
||||
Disable(ctx context.Context) error
|
||||
}
|
||||
|
||||
type httpAuthAPI struct {
|
||||
client httpClient
|
||||
}
|
||||
|
||||
func (s *httpAuthAPI) Enable(ctx context.Context) error {
|
||||
return s.enableDisable(ctx, &authAPIAction{"PUT"})
|
||||
}
|
||||
|
||||
func (s *httpAuthAPI) Disable(ctx context.Context) error {
|
||||
return s.enableDisable(ctx, &authAPIAction{"DELETE"})
|
||||
}
|
||||
|
||||
func (s *httpAuthAPI) enableDisable(ctx context.Context, req httpAction) error {
|
||||
resp, body, err := s.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
|
||||
var sec authError
|
||||
err = json.Unmarshal(body, &sec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type authAPIAction struct {
|
||||
verb string
|
||||
}
|
||||
|
||||
func (l *authAPIAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2AuthURL(ep, "enable", "")
|
||||
req, _ := http.NewRequest(l.verb, u.String(), nil)
|
||||
return req
|
||||
}
|
||||
|
||||
type authError struct {
|
||||
Message string `json:"message"`
|
||||
Code int `json:"-"`
|
||||
}
|
||||
|
||||
func (e authError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// NewAuthUserAPI constructs a new AuthUserAPI that uses HTTP to
|
||||
// interact with etcd's user creation and modification features.
|
||||
func NewAuthUserAPI(c Client) AuthUserAPI {
|
||||
return &httpAuthUserAPI{
|
||||
client: c,
|
||||
}
|
||||
}
|
||||
|
||||
type AuthUserAPI interface {
|
||||
// AddUser adds a user.
|
||||
AddUser(ctx context.Context, username string, password string) error
|
||||
|
||||
// RemoveUser removes a user.
|
||||
RemoveUser(ctx context.Context, username string) error
|
||||
|
||||
// GetUser retrieves user details.
|
||||
GetUser(ctx context.Context, username string) (*User, error)
|
||||
|
||||
// GrantUser grants a user some permission roles.
|
||||
GrantUser(ctx context.Context, username string, roles []string) (*User, error)
|
||||
|
||||
// RevokeUser revokes some permission roles from a user.
|
||||
RevokeUser(ctx context.Context, username string, roles []string) (*User, error)
|
||||
|
||||
// ChangePassword changes the user's password.
|
||||
ChangePassword(ctx context.Context, username string, password string) (*User, error)
|
||||
|
||||
// ListUsers lists the users.
|
||||
ListUsers(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
type httpAuthUserAPI struct {
|
||||
client httpClient
|
||||
}
|
||||
|
||||
type authUserAPIAction struct {
|
||||
verb string
|
||||
username string
|
||||
user *User
|
||||
}
|
||||
|
||||
type authUserAPIList struct{}
|
||||
|
||||
func (list *authUserAPIList) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2AuthURL(ep, "users", "")
|
||||
req, _ := http.NewRequest("GET", u.String(), nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func (l *authUserAPIAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2AuthURL(ep, "users", l.username)
|
||||
if l.user == nil {
|
||||
req, _ := http.NewRequest(l.verb, u.String(), nil)
|
||||
return req
|
||||
}
|
||||
b, err := json.Marshal(l.user)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
body := bytes.NewReader(b)
|
||||
req, _ := http.NewRequest(l.verb, u.String(), body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func (u *httpAuthUserAPI) ListUsers(ctx context.Context) ([]string, error) {
|
||||
resp, body, err := u.client.Do(ctx, &authUserAPIList{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
|
||||
var sec authError
|
||||
err = json.Unmarshal(body, &sec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, sec
|
||||
}
|
||||
|
||||
var userList struct {
|
||||
Users []userListEntry `json:"users"`
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(body, &userList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]string, 0, len(userList.Users))
|
||||
for _, u := range userList.Users {
|
||||
ret = append(ret, u.User)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (u *httpAuthUserAPI) AddUser(ctx context.Context, username string, password string) error {
|
||||
user := &User{
|
||||
User: username,
|
||||
Password: password,
|
||||
}
|
||||
return u.addRemoveUser(ctx, &authUserAPIAction{
|
||||
verb: "PUT",
|
||||
username: username,
|
||||
user: user,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *httpAuthUserAPI) RemoveUser(ctx context.Context, username string) error {
|
||||
return u.addRemoveUser(ctx, &authUserAPIAction{
|
||||
verb: "DELETE",
|
||||
username: username,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *httpAuthUserAPI) addRemoveUser(ctx context.Context, req *authUserAPIAction) error {
|
||||
resp, body, err := u.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
|
||||
var sec authError
|
||||
err = json.Unmarshal(body, &sec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sec
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *httpAuthUserAPI) GetUser(ctx context.Context, username string) (*User, error) {
|
||||
return u.modUser(ctx, &authUserAPIAction{
|
||||
verb: "GET",
|
||||
username: username,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *httpAuthUserAPI) GrantUser(ctx context.Context, username string, roles []string) (*User, error) {
|
||||
user := &User{
|
||||
User: username,
|
||||
Grant: roles,
|
||||
}
|
||||
return u.modUser(ctx, &authUserAPIAction{
|
||||
verb: "PUT",
|
||||
username: username,
|
||||
user: user,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *httpAuthUserAPI) RevokeUser(ctx context.Context, username string, roles []string) (*User, error) {
|
||||
user := &User{
|
||||
User: username,
|
||||
Revoke: roles,
|
||||
}
|
||||
return u.modUser(ctx, &authUserAPIAction{
|
||||
verb: "PUT",
|
||||
username: username,
|
||||
user: user,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *httpAuthUserAPI) ChangePassword(ctx context.Context, username string, password string) (*User, error) {
|
||||
user := &User{
|
||||
User: username,
|
||||
Password: password,
|
||||
}
|
||||
return u.modUser(ctx, &authUserAPIAction{
|
||||
verb: "PUT",
|
||||
username: username,
|
||||
user: user,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *httpAuthUserAPI) modUser(ctx context.Context, req *authUserAPIAction) (*User, error) {
|
||||
resp, body, err := u.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
|
||||
var sec authError
|
||||
err = json.Unmarshal(body, &sec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, sec
|
||||
}
|
||||
var user User
|
||||
if err = json.Unmarshal(body, &user); err != nil {
|
||||
var userR UserRoles
|
||||
if urerr := json.Unmarshal(body, &userR); urerr != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.User = userR.User
|
||||
for _, r := range userR.Roles {
|
||||
user.Roles = append(user.Roles, r.Role)
|
||||
}
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// borrowed from golang/net/context/ctxhttp/cancelreq.go
|
||||
|
||||
package client
|
||||
|
||||
import "net/http"
|
||||
|
||||
func requestCanceler(tr CancelableTransport, req *http.Request) func() {
|
||||
ch := make(chan struct{})
|
||||
req.Cancel = ch
|
||||
|
||||
return func() {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
+704
@@ -0,0 +1,704 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/version"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoEndpoints = errors.New("client: no endpoints available")
|
||||
ErrTooManyRedirects = errors.New("client: too many redirects")
|
||||
ErrClusterUnavailable = errors.New("client: etcd cluster is unavailable or misconfigured")
|
||||
ErrNoLeaderEndpoint = errors.New("client: no leader endpoint available")
|
||||
errTooManyRedirectChecks = errors.New("client: too many redirect checks")
|
||||
|
||||
// oneShotCtxValue is set on a context using WithValue(&oneShotValue) so
|
||||
// that Do() will not retry a request
|
||||
oneShotCtxValue interface{}
|
||||
)
|
||||
|
||||
var DefaultRequestTimeout = 5 * time.Second
|
||||
|
||||
var DefaultTransport CancelableTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
type EndpointSelectionMode int
|
||||
|
||||
const (
|
||||
// EndpointSelectionRandom is the default value of the 'SelectionMode'.
|
||||
// As the name implies, the client object will pick a node from the members
|
||||
// of the cluster in a random fashion. If the cluster has three members, A, B,
|
||||
// and C, the client picks any node from its three members as its request
|
||||
// destination.
|
||||
EndpointSelectionRandom EndpointSelectionMode = iota
|
||||
|
||||
// If 'SelectionMode' is set to 'EndpointSelectionPrioritizeLeader',
|
||||
// requests are sent directly to the cluster leader. This reduces
|
||||
// forwarding roundtrips compared to making requests to etcd followers
|
||||
// who then forward them to the cluster leader. In the event of a leader
|
||||
// failure, however, clients configured this way cannot prioritize among
|
||||
// the remaining etcd followers. Therefore, when a client sets 'SelectionMode'
|
||||
// to 'EndpointSelectionPrioritizeLeader', it must use 'client.AutoSync()' to
|
||||
// maintain its knowledge of current cluster state.
|
||||
//
|
||||
// This mode should be used with Client.AutoSync().
|
||||
EndpointSelectionPrioritizeLeader
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Endpoints defines a set of URLs (schemes, hosts and ports only)
|
||||
// that can be used to communicate with a logical etcd cluster. For
|
||||
// example, a three-node cluster could be provided like so:
|
||||
//
|
||||
// Endpoints: []string{
|
||||
// "http://node1.example.com:2379",
|
||||
// "http://node2.example.com:2379",
|
||||
// "http://node3.example.com:2379",
|
||||
// }
|
||||
//
|
||||
// If multiple endpoints are provided, the Client will attempt to
|
||||
// use them all in the event that one or more of them are unusable.
|
||||
//
|
||||
// If Client.Sync is ever called, the Client may cache an alternate
|
||||
// set of endpoints to continue operation.
|
||||
Endpoints []string
|
||||
|
||||
// Transport is used by the Client to drive HTTP requests. If not
|
||||
// provided, DefaultTransport will be used.
|
||||
Transport CancelableTransport
|
||||
|
||||
// CheckRedirect specifies the policy for handling HTTP redirects.
|
||||
// If CheckRedirect is not nil, the Client calls it before
|
||||
// following an HTTP redirect. The sole argument is the number of
|
||||
// requests that have already been made. If CheckRedirect returns
|
||||
// an error, Client.Do will not make any further requests and return
|
||||
// the error back it to the caller.
|
||||
//
|
||||
// If CheckRedirect is nil, the Client uses its default policy,
|
||||
// which is to stop after 10 consecutive requests.
|
||||
CheckRedirect CheckRedirectFunc
|
||||
|
||||
// Username specifies the user credential to add as an authorization header
|
||||
Username string
|
||||
|
||||
// Password is the password for the specified user to add as an authorization header
|
||||
// to the request.
|
||||
Password string
|
||||
|
||||
// HeaderTimeoutPerRequest specifies the time limit to wait for response
|
||||
// header in a single request made by the Client. The timeout includes
|
||||
// connection time, any redirects, and header wait time.
|
||||
//
|
||||
// For non-watch GET request, server returns the response body immediately.
|
||||
// For PUT/POST/DELETE request, server will attempt to commit request
|
||||
// before responding, which is expected to take `100ms + 2 * RTT`.
|
||||
// For watch request, server returns the header immediately to notify Client
|
||||
// watch start. But if server is behind some kind of proxy, the response
|
||||
// header may be cached at proxy, and Client cannot rely on this behavior.
|
||||
//
|
||||
// Especially, wait request will ignore this timeout.
|
||||
//
|
||||
// One API call may send multiple requests to different etcd servers until it
|
||||
// succeeds. Use context of the API to specify the overall timeout.
|
||||
//
|
||||
// A HeaderTimeoutPerRequest of zero means no timeout.
|
||||
HeaderTimeoutPerRequest time.Duration
|
||||
|
||||
// SelectionMode is an EndpointSelectionMode enum that specifies the
|
||||
// policy for choosing the etcd cluster node to which requests are sent.
|
||||
SelectionMode EndpointSelectionMode
|
||||
}
|
||||
|
||||
func (cfg *Config) transport() CancelableTransport {
|
||||
if cfg.Transport == nil {
|
||||
return DefaultTransport
|
||||
}
|
||||
return cfg.Transport
|
||||
}
|
||||
|
||||
func (cfg *Config) checkRedirect() CheckRedirectFunc {
|
||||
if cfg.CheckRedirect == nil {
|
||||
return DefaultCheckRedirect
|
||||
}
|
||||
return cfg.CheckRedirect
|
||||
}
|
||||
|
||||
// CancelableTransport mimics net/http.Transport, but requires that
|
||||
// the object also support request cancellation.
|
||||
type CancelableTransport interface {
|
||||
http.RoundTripper
|
||||
CancelRequest(req *http.Request)
|
||||
}
|
||||
|
||||
type CheckRedirectFunc func(via int) error
|
||||
|
||||
// DefaultCheckRedirect follows up to 10 redirects, but no more.
|
||||
var DefaultCheckRedirect CheckRedirectFunc = func(via int) error {
|
||||
if via > 10 {
|
||||
return ErrTooManyRedirects
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
// Sync updates the internal cache of the etcd cluster's membership.
|
||||
Sync(context.Context) error
|
||||
|
||||
// AutoSync periodically calls Sync() every given interval.
|
||||
// The recommended sync interval is 10 seconds to 1 minute, which does
|
||||
// not bring too much overhead to server and makes client catch up the
|
||||
// cluster change in time.
|
||||
//
|
||||
// The example to use it:
|
||||
//
|
||||
// for {
|
||||
// err := client.AutoSync(ctx, 10*time.Second)
|
||||
// if err == context.DeadlineExceeded || err == context.Canceled {
|
||||
// break
|
||||
// }
|
||||
// log.Print(err)
|
||||
// }
|
||||
AutoSync(context.Context, time.Duration) error
|
||||
|
||||
// Endpoints returns a copy of the current set of API endpoints used
|
||||
// by Client to resolve HTTP requests. If Sync has ever been called,
|
||||
// this may differ from the initial Endpoints provided in the Config.
|
||||
Endpoints() []string
|
||||
|
||||
// SetEndpoints sets the set of API endpoints used by Client to resolve
|
||||
// HTTP requests. If the given endpoints are not valid, an error will be
|
||||
// returned
|
||||
SetEndpoints(eps []string) error
|
||||
|
||||
// GetVersion retrieves the current etcd server and cluster version
|
||||
GetVersion(ctx context.Context) (*version.Versions, error)
|
||||
|
||||
httpClient
|
||||
}
|
||||
|
||||
func New(cfg Config) (Client, error) {
|
||||
c := &httpClusterClient{
|
||||
clientFactory: newHTTPClientFactory(cfg.transport(), cfg.checkRedirect(), cfg.HeaderTimeoutPerRequest),
|
||||
rand: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
|
||||
selectionMode: cfg.SelectionMode,
|
||||
}
|
||||
if cfg.Username != "" {
|
||||
c.credentials = &credentials{
|
||||
username: cfg.Username,
|
||||
password: cfg.Password,
|
||||
}
|
||||
}
|
||||
if err := c.SetEndpoints(cfg.Endpoints); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type httpClient interface {
|
||||
Do(context.Context, httpAction) (*http.Response, []byte, error)
|
||||
}
|
||||
|
||||
func newHTTPClientFactory(tr CancelableTransport, cr CheckRedirectFunc, headerTimeout time.Duration) httpClientFactory {
|
||||
return func(ep url.URL) httpClient {
|
||||
return &redirectFollowingHTTPClient{
|
||||
checkRedirect: cr,
|
||||
client: &simpleHTTPClient{
|
||||
transport: tr,
|
||||
endpoint: ep,
|
||||
headerTimeout: headerTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type credentials struct {
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
type httpClientFactory func(url.URL) httpClient
|
||||
|
||||
type httpAction interface {
|
||||
HTTPRequest(url.URL) *http.Request
|
||||
}
|
||||
|
||||
type httpClusterClient struct {
|
||||
clientFactory httpClientFactory
|
||||
endpoints []url.URL
|
||||
pinned int
|
||||
credentials *credentials
|
||||
sync.RWMutex
|
||||
rand *rand.Rand
|
||||
selectionMode EndpointSelectionMode
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) getLeaderEndpoint(ctx context.Context, eps []url.URL) (string, error) {
|
||||
ceps := make([]url.URL, len(eps))
|
||||
copy(ceps, eps)
|
||||
|
||||
// To perform a lookup on the new endpoint list without using the current
|
||||
// client, we'll copy it
|
||||
clientCopy := &httpClusterClient{
|
||||
clientFactory: c.clientFactory,
|
||||
credentials: c.credentials,
|
||||
rand: c.rand,
|
||||
|
||||
pinned: 0,
|
||||
endpoints: ceps,
|
||||
}
|
||||
|
||||
mAPI := NewMembersAPI(clientCopy)
|
||||
leader, err := mAPI.Leader(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(leader.ClientURLs) == 0 {
|
||||
return "", ErrNoLeaderEndpoint
|
||||
}
|
||||
|
||||
return leader.ClientURLs[0], nil // TODO: how to handle multiple client URLs?
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) parseEndpoints(eps []string) ([]url.URL, error) {
|
||||
if len(eps) == 0 {
|
||||
return []url.URL{}, ErrNoEndpoints
|
||||
}
|
||||
|
||||
neps := make([]url.URL, len(eps))
|
||||
for i, ep := range eps {
|
||||
u, err := url.Parse(ep)
|
||||
if err != nil {
|
||||
return []url.URL{}, err
|
||||
}
|
||||
neps[i] = *u
|
||||
}
|
||||
return neps, nil
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) SetEndpoints(eps []string) error {
|
||||
neps, err := c.parseEndpoints(eps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.endpoints = shuffleEndpoints(c.rand, neps)
|
||||
// We're not doing anything for PrioritizeLeader here. This is
|
||||
// due to not having a context meaning we can't call getLeaderEndpoint
|
||||
// However, if you're using PrioritizeLeader, you've already been told
|
||||
// to regularly call sync, where we do have a ctx, and can figure the
|
||||
// leader. PrioritizeLeader is also quite a loose guarantee, so deal
|
||||
// with it
|
||||
c.pinned = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
|
||||
action := act
|
||||
c.RLock()
|
||||
leps := len(c.endpoints)
|
||||
eps := make([]url.URL, leps)
|
||||
n := copy(eps, c.endpoints)
|
||||
pinned := c.pinned
|
||||
|
||||
if c.credentials != nil {
|
||||
action = &authedAction{
|
||||
act: act,
|
||||
credentials: *c.credentials,
|
||||
}
|
||||
}
|
||||
c.RUnlock()
|
||||
|
||||
if leps == 0 {
|
||||
return nil, nil, ErrNoEndpoints
|
||||
}
|
||||
|
||||
if leps != n {
|
||||
return nil, nil, errors.New("unable to pick endpoint: copy failed")
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var body []byte
|
||||
var err error
|
||||
cerr := &ClusterError{}
|
||||
isOneShot := ctx.Value(&oneShotCtxValue) != nil
|
||||
|
||||
for i := pinned; i < leps+pinned; i++ {
|
||||
k := i % leps
|
||||
hc := c.clientFactory(eps[k])
|
||||
resp, body, err = hc.Do(ctx, action)
|
||||
if err != nil {
|
||||
cerr.Errors = append(cerr.Errors, err)
|
||||
if err == ctx.Err() {
|
||||
return nil, nil, ctx.Err()
|
||||
}
|
||||
if err == context.Canceled || err == context.DeadlineExceeded {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if resp.StatusCode/100 == 5 {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusInternalServerError, http.StatusServiceUnavailable:
|
||||
// TODO: make sure this is a no leader response
|
||||
cerr.Errors = append(cerr.Errors, fmt.Errorf("client: etcd member %s has no leader", eps[k].String()))
|
||||
default:
|
||||
cerr.Errors = append(cerr.Errors, fmt.Errorf("client: etcd member %s returns server error [%s]", eps[k].String(), http.StatusText(resp.StatusCode)))
|
||||
}
|
||||
err = cerr.Errors[0]
|
||||
}
|
||||
if err != nil {
|
||||
if !isOneShot {
|
||||
continue
|
||||
}
|
||||
c.Lock()
|
||||
c.pinned = (k + 1) % leps
|
||||
c.Unlock()
|
||||
return nil, nil, err
|
||||
}
|
||||
if k != pinned {
|
||||
c.Lock()
|
||||
c.pinned = k
|
||||
c.Unlock()
|
||||
}
|
||||
return resp, body, nil
|
||||
}
|
||||
|
||||
return nil, nil, cerr
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) Endpoints() []string {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
eps := make([]string, len(c.endpoints))
|
||||
for i, ep := range c.endpoints {
|
||||
eps[i] = ep.String()
|
||||
}
|
||||
|
||||
return eps
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) Sync(ctx context.Context) error {
|
||||
mAPI := NewMembersAPI(c)
|
||||
ms, err := mAPI.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var eps []string
|
||||
for _, m := range ms {
|
||||
eps = append(eps, m.ClientURLs...)
|
||||
}
|
||||
|
||||
neps, err := c.parseEndpoints(eps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
npin := 0
|
||||
|
||||
switch c.selectionMode {
|
||||
case EndpointSelectionRandom:
|
||||
c.RLock()
|
||||
eq := endpointsEqual(c.endpoints, neps)
|
||||
c.RUnlock()
|
||||
|
||||
if eq {
|
||||
return nil
|
||||
}
|
||||
// When items in the endpoint list changes, we choose a new pin
|
||||
neps = shuffleEndpoints(c.rand, neps)
|
||||
case EndpointSelectionPrioritizeLeader:
|
||||
nle, err := c.getLeaderEndpoint(ctx, neps)
|
||||
if err != nil {
|
||||
return ErrNoLeaderEndpoint
|
||||
}
|
||||
|
||||
for i, n := range neps {
|
||||
if n.String() == nle {
|
||||
npin = i
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid endpoint selection mode: %d", c.selectionMode)
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.endpoints = neps
|
||||
c.pinned = npin
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) AutoSync(ctx context.Context, interval time.Duration) error {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
err := c.Sync(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *httpClusterClient) GetVersion(ctx context.Context) (*version.Versions, error) {
|
||||
act := &getAction{Prefix: "/version"}
|
||||
|
||||
resp, body, err := c.Do(ctx, act)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
if len(body) == 0 {
|
||||
return nil, ErrEmptyBody
|
||||
}
|
||||
var vresp version.Versions
|
||||
if err := json.Unmarshal(body, &vresp); err != nil {
|
||||
return nil, ErrInvalidJSON
|
||||
}
|
||||
return &vresp, nil
|
||||
default:
|
||||
var etcdErr Error
|
||||
if err := json.Unmarshal(body, &etcdErr); err != nil {
|
||||
return nil, ErrInvalidJSON
|
||||
}
|
||||
return nil, etcdErr
|
||||
}
|
||||
}
|
||||
|
||||
type roundTripResponse struct {
|
||||
resp *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
type simpleHTTPClient struct {
|
||||
transport CancelableTransport
|
||||
endpoint url.URL
|
||||
headerTimeout time.Duration
|
||||
}
|
||||
|
||||
func (c *simpleHTTPClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
|
||||
req := act.HTTPRequest(c.endpoint)
|
||||
|
||||
if err := printcURL(req); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
isWait := false
|
||||
if req != nil && req.URL != nil {
|
||||
ws := req.URL.Query().Get("wait")
|
||||
if len(ws) != 0 {
|
||||
var err error
|
||||
isWait, err = strconv.ParseBool(ws)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("wrong wait value %s (%v for %+v)", ws, err, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hctx context.Context
|
||||
var hcancel context.CancelFunc
|
||||
if !isWait && c.headerTimeout > 0 {
|
||||
hctx, hcancel = context.WithTimeout(ctx, c.headerTimeout)
|
||||
} else {
|
||||
hctx, hcancel = context.WithCancel(ctx)
|
||||
}
|
||||
defer hcancel()
|
||||
|
||||
reqcancel := requestCanceler(c.transport, req)
|
||||
|
||||
rtchan := make(chan roundTripResponse, 1)
|
||||
go func() {
|
||||
resp, err := c.transport.RoundTrip(req)
|
||||
rtchan <- roundTripResponse{resp: resp, err: err}
|
||||
close(rtchan)
|
||||
}()
|
||||
|
||||
var resp *http.Response
|
||||
var err error
|
||||
|
||||
select {
|
||||
case rtresp := <-rtchan:
|
||||
resp, err = rtresp.resp, rtresp.err
|
||||
case <-hctx.Done():
|
||||
// cancel and wait for request to actually exit before continuing
|
||||
reqcancel()
|
||||
rtresp := <-rtchan
|
||||
resp = rtresp.resp
|
||||
switch {
|
||||
case ctx.Err() != nil:
|
||||
err = ctx.Err()
|
||||
case hctx.Err() != nil:
|
||||
err = fmt.Errorf("client: endpoint %s exceeded header timeout", c.endpoint.String())
|
||||
default:
|
||||
panic("failed to get error from context")
|
||||
}
|
||||
}
|
||||
|
||||
// always check for resp nil-ness to deal with possible
|
||||
// race conditions between channels above
|
||||
defer func() {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var body []byte
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
resp.Body.Close()
|
||||
<-done
|
||||
return nil, nil, ctx.Err()
|
||||
case <-done:
|
||||
}
|
||||
|
||||
return resp, body, err
|
||||
}
|
||||
|
||||
type authedAction struct {
|
||||
act httpAction
|
||||
credentials credentials
|
||||
}
|
||||
|
||||
func (a *authedAction) HTTPRequest(url url.URL) *http.Request {
|
||||
r := a.act.HTTPRequest(url)
|
||||
r.SetBasicAuth(a.credentials.username, a.credentials.password)
|
||||
return r
|
||||
}
|
||||
|
||||
type redirectFollowingHTTPClient struct {
|
||||
client httpClient
|
||||
checkRedirect CheckRedirectFunc
|
||||
}
|
||||
|
||||
func (r *redirectFollowingHTTPClient) Do(ctx context.Context, act httpAction) (*http.Response, []byte, error) {
|
||||
next := act
|
||||
for i := 0; i < 100; i++ {
|
||||
if i > 0 {
|
||||
if err := r.checkRedirect(i); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
resp, body, err := r.client.Do(ctx, next)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode/100 == 3 {
|
||||
hdr := resp.Header.Get("Location")
|
||||
if hdr == "" {
|
||||
return nil, nil, fmt.Errorf("Location header not set")
|
||||
}
|
||||
loc, err := url.Parse(hdr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Location header not valid URL: %s", hdr)
|
||||
}
|
||||
next = &redirectedHTTPAction{
|
||||
action: act,
|
||||
location: *loc,
|
||||
}
|
||||
continue
|
||||
}
|
||||
return resp, body, nil
|
||||
}
|
||||
|
||||
return nil, nil, errTooManyRedirectChecks
|
||||
}
|
||||
|
||||
type redirectedHTTPAction struct {
|
||||
action httpAction
|
||||
location url.URL
|
||||
}
|
||||
|
||||
func (r *redirectedHTTPAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
orig := r.action.HTTPRequest(ep)
|
||||
orig.URL = &r.location
|
||||
return orig
|
||||
}
|
||||
|
||||
func shuffleEndpoints(r *rand.Rand, eps []url.URL) []url.URL {
|
||||
p := r.Perm(len(eps))
|
||||
neps := make([]url.URL, len(eps))
|
||||
for i, k := range p {
|
||||
neps[i] = eps[k]
|
||||
}
|
||||
return neps
|
||||
}
|
||||
|
||||
func endpointsEqual(left, right []url.URL) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
|
||||
sLeft := make([]string, len(left))
|
||||
sRight := make([]string, len(right))
|
||||
for i, l := range left {
|
||||
sLeft[i] = l.String()
|
||||
}
|
||||
for i, r := range right {
|
||||
sRight[i] = r.String()
|
||||
}
|
||||
|
||||
sort.Strings(sLeft)
|
||||
sort.Strings(sRight)
|
||||
for i := range sLeft {
|
||||
if sLeft[i] != sRight[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 client
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ClusterError struct {
|
||||
Errors []error
|
||||
}
|
||||
|
||||
func (ce *ClusterError) Error() string {
|
||||
s := ErrClusterUnavailable.Error()
|
||||
for i, e := range ce.Errors {
|
||||
s += fmt.Sprintf("; error #%d: %s\n", i, e)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (ce *ClusterError) Detail() string {
|
||||
s := ""
|
||||
for i, e := range ce.Errors {
|
||||
s += fmt.Sprintf("error #%d: %s\n", i, e)
|
||||
}
|
||||
return s
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
cURLDebug = false
|
||||
)
|
||||
|
||||
func EnablecURLDebug() {
|
||||
cURLDebug = true
|
||||
}
|
||||
|
||||
func DisablecURLDebug() {
|
||||
cURLDebug = false
|
||||
}
|
||||
|
||||
// printcURL prints the cURL equivalent request to stderr.
|
||||
// It returns an error if the body of the request cannot
|
||||
// be read.
|
||||
// The caller MUST cancel the request if there is an error.
|
||||
func printcURL(req *http.Request) error {
|
||||
if !cURLDebug {
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
command string
|
||||
b []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if req.URL != nil {
|
||||
command = fmt.Sprintf("curl -X %s %s", req.Method, req.URL.String())
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
b, err = ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command += fmt.Sprintf(" -d %q", string(b))
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "cURL Command: %s\n", command)
|
||||
|
||||
// reset body
|
||||
body := bytes.NewBuffer(b)
|
||||
req.Body = ioutil.NopCloser(body)
|
||||
|
||||
return nil
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 client
|
||||
|
||||
import (
|
||||
"github.com/coreos/etcd/pkg/srv"
|
||||
)
|
||||
|
||||
// Discoverer is an interface that wraps the Discover method.
|
||||
type Discoverer interface {
|
||||
// Discover looks up the etcd servers for the domain.
|
||||
Discover(domain string) ([]string, error)
|
||||
}
|
||||
|
||||
type srvDiscover struct{}
|
||||
|
||||
// NewSRVDiscover constructs a new Discoverer that uses the stdlib to lookup SRV records.
|
||||
func NewSRVDiscover() Discoverer {
|
||||
return &srvDiscover{}
|
||||
}
|
||||
|
||||
func (d *srvDiscover) Discover(domain string) ([]string, error) {
|
||||
srvs, err := srv.GetClient("etcd-client", domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return srvs.Endpoints, nil
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 client provides bindings for the etcd APIs.
|
||||
|
||||
Create a Config and exchange it for a Client:
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
cfg := client.Config{
|
||||
Endpoints: []string{"http://127.0.0.1:2379"},
|
||||
Transport: DefaultTransport,
|
||||
}
|
||||
|
||||
c, err := client.New(cfg)
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
Clients are safe for concurrent use by multiple goroutines.
|
||||
|
||||
Create a KeysAPI using the Client, then use it to interact with etcd:
|
||||
|
||||
kAPI := client.NewKeysAPI(c)
|
||||
|
||||
// create a new key /foo with the value "bar"
|
||||
_, err = kAPI.Create(context.Background(), "/foo", "bar")
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
// delete the newly created key only if the value is still "bar"
|
||||
_, err = kAPI.Delete(context.Background(), "/foo", &DeleteOptions{PrevValue: "bar"})
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
Use a custom context to set timeouts on your operations:
|
||||
|
||||
import "time"
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// set a new key, ignoring it's previous state
|
||||
_, err := kAPI.Set(ctx, "/ping", "pong", nil)
|
||||
if err != nil {
|
||||
if err == context.DeadlineExceeded {
|
||||
// request took longer than 5s
|
||||
} else {
|
||||
// handle error
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
package client
|
||||
+1087
File diff suppressed because it is too large
Load Diff
+682
@@ -0,0 +1,682 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 client
|
||||
|
||||
//go:generate codecgen -d 1819 -r "Node|Response|Nodes" -o keys.generated.go keys.go
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/pkg/pathutil"
|
||||
"github.com/ugorji/go/codec"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrorCodeKeyNotFound = 100
|
||||
ErrorCodeTestFailed = 101
|
||||
ErrorCodeNotFile = 102
|
||||
ErrorCodeNotDir = 104
|
||||
ErrorCodeNodeExist = 105
|
||||
ErrorCodeRootROnly = 107
|
||||
ErrorCodeDirNotEmpty = 108
|
||||
ErrorCodeUnauthorized = 110
|
||||
|
||||
ErrorCodePrevValueRequired = 201
|
||||
ErrorCodeTTLNaN = 202
|
||||
ErrorCodeIndexNaN = 203
|
||||
ErrorCodeInvalidField = 209
|
||||
ErrorCodeInvalidForm = 210
|
||||
|
||||
ErrorCodeRaftInternal = 300
|
||||
ErrorCodeLeaderElect = 301
|
||||
|
||||
ErrorCodeWatcherCleared = 400
|
||||
ErrorCodeEventIndexCleared = 401
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"errorCode"`
|
||||
Message string `json:"message"`
|
||||
Cause string `json:"cause"`
|
||||
Index uint64 `json:"index"`
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("%v: %v (%v) [%v]", e.Code, e.Message, e.Cause, e.Index)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidJSON = errors.New("client: response is invalid json. The endpoint is probably not valid etcd cluster endpoint.")
|
||||
ErrEmptyBody = errors.New("client: response body is empty")
|
||||
)
|
||||
|
||||
// PrevExistType is used to define an existence condition when setting
|
||||
// or deleting Nodes.
|
||||
type PrevExistType string
|
||||
|
||||
const (
|
||||
PrevIgnore = PrevExistType("")
|
||||
PrevExist = PrevExistType("true")
|
||||
PrevNoExist = PrevExistType("false")
|
||||
)
|
||||
|
||||
var (
|
||||
defaultV2KeysPrefix = "/v2/keys"
|
||||
)
|
||||
|
||||
// NewKeysAPI builds a KeysAPI that interacts with etcd's key-value
|
||||
// API over HTTP.
|
||||
func NewKeysAPI(c Client) KeysAPI {
|
||||
return NewKeysAPIWithPrefix(c, defaultV2KeysPrefix)
|
||||
}
|
||||
|
||||
// NewKeysAPIWithPrefix acts like NewKeysAPI, but allows the caller
|
||||
// to provide a custom base URL path. This should only be used in
|
||||
// very rare cases.
|
||||
func NewKeysAPIWithPrefix(c Client, p string) KeysAPI {
|
||||
return &httpKeysAPI{
|
||||
client: c,
|
||||
prefix: p,
|
||||
}
|
||||
}
|
||||
|
||||
type KeysAPI interface {
|
||||
// Get retrieves a set of Nodes from etcd
|
||||
Get(ctx context.Context, key string, opts *GetOptions) (*Response, error)
|
||||
|
||||
// Set assigns a new value to a Node identified by a given key. The caller
|
||||
// may define a set of conditions in the SetOptions. If SetOptions.Dir=true
|
||||
// then value is ignored.
|
||||
Set(ctx context.Context, key, value string, opts *SetOptions) (*Response, error)
|
||||
|
||||
// Delete removes a Node identified by the given key, optionally destroying
|
||||
// all of its children as well. The caller may define a set of required
|
||||
// conditions in an DeleteOptions object.
|
||||
Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error)
|
||||
|
||||
// Create is an alias for Set w/ PrevExist=false
|
||||
Create(ctx context.Context, key, value string) (*Response, error)
|
||||
|
||||
// CreateInOrder is used to atomically create in-order keys within the given directory.
|
||||
CreateInOrder(ctx context.Context, dir, value string, opts *CreateInOrderOptions) (*Response, error)
|
||||
|
||||
// Update is an alias for Set w/ PrevExist=true
|
||||
Update(ctx context.Context, key, value string) (*Response, error)
|
||||
|
||||
// Watcher builds a new Watcher targeted at a specific Node identified
|
||||
// by the given key. The Watcher may be configured at creation time
|
||||
// through a WatcherOptions object. The returned Watcher is designed
|
||||
// to emit events that happen to a Node, and optionally to its children.
|
||||
Watcher(key string, opts *WatcherOptions) Watcher
|
||||
}
|
||||
|
||||
type WatcherOptions struct {
|
||||
// AfterIndex defines the index after-which the Watcher should
|
||||
// start emitting events. For example, if a value of 5 is
|
||||
// provided, the first event will have an index >= 6.
|
||||
//
|
||||
// Setting AfterIndex to 0 (default) means that the Watcher
|
||||
// should start watching for events starting at the current
|
||||
// index, whatever that may be.
|
||||
AfterIndex uint64
|
||||
|
||||
// Recursive specifies whether or not the Watcher should emit
|
||||
// events that occur in children of the given keyspace. If set
|
||||
// to false (default), events will be limited to those that
|
||||
// occur for the exact key.
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
type CreateInOrderOptions struct {
|
||||
// TTL defines a period of time after-which the Node should
|
||||
// expire and no longer exist. Values <= 0 are ignored. Given
|
||||
// that the zero-value is ignored, TTL cannot be used to set
|
||||
// a TTL of 0.
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
type SetOptions struct {
|
||||
// PrevValue specifies what the current value of the Node must
|
||||
// be in order for the Set operation to succeed.
|
||||
//
|
||||
// Leaving this field empty means that the caller wishes to
|
||||
// ignore the current value of the Node. This cannot be used
|
||||
// to compare the Node's current value to an empty string.
|
||||
//
|
||||
// PrevValue is ignored if Dir=true
|
||||
PrevValue string
|
||||
|
||||
// PrevIndex indicates what the current ModifiedIndex of the
|
||||
// Node must be in order for the Set operation to succeed.
|
||||
//
|
||||
// If PrevIndex is set to 0 (default), no comparison is made.
|
||||
PrevIndex uint64
|
||||
|
||||
// PrevExist specifies whether the Node must currently exist
|
||||
// (PrevExist) or not (PrevNoExist). If the caller does not
|
||||
// care about existence, set PrevExist to PrevIgnore, or simply
|
||||
// leave it unset.
|
||||
PrevExist PrevExistType
|
||||
|
||||
// TTL defines a period of time after-which the Node should
|
||||
// expire and no longer exist. Values <= 0 are ignored. Given
|
||||
// that the zero-value is ignored, TTL cannot be used to set
|
||||
// a TTL of 0.
|
||||
TTL time.Duration
|
||||
|
||||
// Refresh set to true means a TTL value can be updated
|
||||
// without firing a watch or changing the node value. A
|
||||
// value must not be provided when refreshing a key.
|
||||
Refresh bool
|
||||
|
||||
// Dir specifies whether or not this Node should be created as a directory.
|
||||
Dir bool
|
||||
|
||||
// NoValueOnSuccess specifies whether the response contains the current value of the Node.
|
||||
// If set, the response will only contain the current value when the request fails.
|
||||
NoValueOnSuccess bool
|
||||
}
|
||||
|
||||
type GetOptions struct {
|
||||
// Recursive defines whether or not all children of the Node
|
||||
// should be returned.
|
||||
Recursive bool
|
||||
|
||||
// Sort instructs the server whether or not to sort the Nodes.
|
||||
// If true, the Nodes are sorted alphabetically by key in
|
||||
// ascending order (A to z). If false (default), the Nodes will
|
||||
// not be sorted and the ordering used should not be considered
|
||||
// predictable.
|
||||
Sort bool
|
||||
|
||||
// Quorum specifies whether it gets the latest committed value that
|
||||
// has been applied in quorum of members, which ensures external
|
||||
// consistency (or linearizability).
|
||||
Quorum bool
|
||||
}
|
||||
|
||||
type DeleteOptions struct {
|
||||
// PrevValue specifies what the current value of the Node must
|
||||
// be in order for the Delete operation to succeed.
|
||||
//
|
||||
// Leaving this field empty means that the caller wishes to
|
||||
// ignore the current value of the Node. This cannot be used
|
||||
// to compare the Node's current value to an empty string.
|
||||
PrevValue string
|
||||
|
||||
// PrevIndex indicates what the current ModifiedIndex of the
|
||||
// Node must be in order for the Delete operation to succeed.
|
||||
//
|
||||
// If PrevIndex is set to 0 (default), no comparison is made.
|
||||
PrevIndex uint64
|
||||
|
||||
// Recursive defines whether or not all children of the Node
|
||||
// should be deleted. If set to true, all children of the Node
|
||||
// identified by the given key will be deleted. If left unset
|
||||
// or explicitly set to false, only a single Node will be
|
||||
// deleted.
|
||||
Recursive bool
|
||||
|
||||
// Dir specifies whether or not this Node should be removed as a directory.
|
||||
Dir bool
|
||||
}
|
||||
|
||||
type Watcher interface {
|
||||
// Next blocks until an etcd event occurs, then returns a Response
|
||||
// representing that event. The behavior of Next depends on the
|
||||
// WatcherOptions used to construct the Watcher. Next is designed to
|
||||
// be called repeatedly, each time blocking until a subsequent event
|
||||
// is available.
|
||||
//
|
||||
// If the provided context is cancelled, Next will return a non-nil
|
||||
// error. Any other failures encountered while waiting for the next
|
||||
// event (connection issues, deserialization failures, etc) will
|
||||
// also result in a non-nil error.
|
||||
Next(context.Context) (*Response, error)
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
// Action is the name of the operation that occurred. Possible values
|
||||
// include get, set, delete, update, create, compareAndSwap,
|
||||
// compareAndDelete and expire.
|
||||
Action string `json:"action"`
|
||||
|
||||
// Node represents the state of the relevant etcd Node.
|
||||
Node *Node `json:"node"`
|
||||
|
||||
// PrevNode represents the previous state of the Node. PrevNode is non-nil
|
||||
// only if the Node existed before the action occurred and the action
|
||||
// caused a change to the Node.
|
||||
PrevNode *Node `json:"prevNode"`
|
||||
|
||||
// Index holds the cluster-level index at the time the Response was generated.
|
||||
// This index is not tied to the Node(s) contained in this Response.
|
||||
Index uint64 `json:"-"`
|
||||
|
||||
// ClusterID holds the cluster-level ID reported by the server. This
|
||||
// should be different for different etcd clusters.
|
||||
ClusterID string `json:"-"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
// Key represents the unique location of this Node (e.g. "/foo/bar").
|
||||
Key string `json:"key"`
|
||||
|
||||
// Dir reports whether node describes a directory.
|
||||
Dir bool `json:"dir,omitempty"`
|
||||
|
||||
// Value is the current data stored on this Node. If this Node
|
||||
// is a directory, Value will be empty.
|
||||
Value string `json:"value"`
|
||||
|
||||
// Nodes holds the children of this Node, only if this Node is a directory.
|
||||
// This slice of will be arbitrarily deep (children, grandchildren, great-
|
||||
// grandchildren, etc.) if a recursive Get or Watch request were made.
|
||||
Nodes Nodes `json:"nodes"`
|
||||
|
||||
// CreatedIndex is the etcd index at-which this Node was created.
|
||||
CreatedIndex uint64 `json:"createdIndex"`
|
||||
|
||||
// ModifiedIndex is the etcd index at-which this Node was last modified.
|
||||
ModifiedIndex uint64 `json:"modifiedIndex"`
|
||||
|
||||
// Expiration is the server side expiration time of the key.
|
||||
Expiration *time.Time `json:"expiration,omitempty"`
|
||||
|
||||
// TTL is the time to live of the key in second.
|
||||
TTL int64 `json:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
func (n *Node) String() string {
|
||||
return fmt.Sprintf("{Key: %s, CreatedIndex: %d, ModifiedIndex: %d, TTL: %d}", n.Key, n.CreatedIndex, n.ModifiedIndex, n.TTL)
|
||||
}
|
||||
|
||||
// TTLDuration returns the Node's TTL as a time.Duration object
|
||||
func (n *Node) TTLDuration() time.Duration {
|
||||
return time.Duration(n.TTL) * time.Second
|
||||
}
|
||||
|
||||
type Nodes []*Node
|
||||
|
||||
// interfaces for sorting
|
||||
|
||||
func (ns Nodes) Len() int { return len(ns) }
|
||||
func (ns Nodes) Less(i, j int) bool { return ns[i].Key < ns[j].Key }
|
||||
func (ns Nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] }
|
||||
|
||||
type httpKeysAPI struct {
|
||||
client httpClient
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (k *httpKeysAPI) Set(ctx context.Context, key, val string, opts *SetOptions) (*Response, error) {
|
||||
act := &setAction{
|
||||
Prefix: k.prefix,
|
||||
Key: key,
|
||||
Value: val,
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
act.PrevValue = opts.PrevValue
|
||||
act.PrevIndex = opts.PrevIndex
|
||||
act.PrevExist = opts.PrevExist
|
||||
act.TTL = opts.TTL
|
||||
act.Refresh = opts.Refresh
|
||||
act.Dir = opts.Dir
|
||||
act.NoValueOnSuccess = opts.NoValueOnSuccess
|
||||
}
|
||||
|
||||
doCtx := ctx
|
||||
if act.PrevExist == PrevNoExist {
|
||||
doCtx = context.WithValue(doCtx, &oneShotCtxValue, &oneShotCtxValue)
|
||||
}
|
||||
resp, body, err := k.client.Do(doCtx, act)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
|
||||
}
|
||||
|
||||
func (k *httpKeysAPI) Create(ctx context.Context, key, val string) (*Response, error) {
|
||||
return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevNoExist})
|
||||
}
|
||||
|
||||
func (k *httpKeysAPI) CreateInOrder(ctx context.Context, dir, val string, opts *CreateInOrderOptions) (*Response, error) {
|
||||
act := &createInOrderAction{
|
||||
Prefix: k.prefix,
|
||||
Dir: dir,
|
||||
Value: val,
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
act.TTL = opts.TTL
|
||||
}
|
||||
|
||||
resp, body, err := k.client.Do(ctx, act)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
|
||||
}
|
||||
|
||||
func (k *httpKeysAPI) Update(ctx context.Context, key, val string) (*Response, error) {
|
||||
return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevExist})
|
||||
}
|
||||
|
||||
func (k *httpKeysAPI) Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error) {
|
||||
act := &deleteAction{
|
||||
Prefix: k.prefix,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
act.PrevValue = opts.PrevValue
|
||||
act.PrevIndex = opts.PrevIndex
|
||||
act.Dir = opts.Dir
|
||||
act.Recursive = opts.Recursive
|
||||
}
|
||||
|
||||
doCtx := context.WithValue(ctx, &oneShotCtxValue, &oneShotCtxValue)
|
||||
resp, body, err := k.client.Do(doCtx, act)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
|
||||
}
|
||||
|
||||
func (k *httpKeysAPI) Get(ctx context.Context, key string, opts *GetOptions) (*Response, error) {
|
||||
act := &getAction{
|
||||
Prefix: k.prefix,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
act.Recursive = opts.Recursive
|
||||
act.Sorted = opts.Sort
|
||||
act.Quorum = opts.Quorum
|
||||
}
|
||||
|
||||
resp, body, err := k.client.Do(ctx, act)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
|
||||
}
|
||||
|
||||
func (k *httpKeysAPI) Watcher(key string, opts *WatcherOptions) Watcher {
|
||||
act := waitAction{
|
||||
Prefix: k.prefix,
|
||||
Key: key,
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
act.Recursive = opts.Recursive
|
||||
if opts.AfterIndex > 0 {
|
||||
act.WaitIndex = opts.AfterIndex + 1
|
||||
}
|
||||
}
|
||||
|
||||
return &httpWatcher{
|
||||
client: k.client,
|
||||
nextWait: act,
|
||||
}
|
||||
}
|
||||
|
||||
type httpWatcher struct {
|
||||
client httpClient
|
||||
nextWait waitAction
|
||||
}
|
||||
|
||||
func (hw *httpWatcher) Next(ctx context.Context) (*Response, error) {
|
||||
for {
|
||||
httpresp, body, err := hw.client.Do(ctx, &hw.nextWait)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := unmarshalHTTPResponse(httpresp.StatusCode, httpresp.Header, body)
|
||||
if err != nil {
|
||||
if err == ErrEmptyBody {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hw.nextWait.WaitIndex = resp.Node.ModifiedIndex + 1
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// v2KeysURL forms a URL representing the location of a key.
|
||||
// The endpoint argument represents the base URL of an etcd
|
||||
// server. The prefix is the path needed to route from the
|
||||
// provided endpoint's path to the root of the keys API
|
||||
// (typically "/v2/keys").
|
||||
func v2KeysURL(ep url.URL, prefix, key string) *url.URL {
|
||||
// We concatenate all parts together manually. We cannot use
|
||||
// path.Join because it does not reserve trailing slash.
|
||||
// We call CanonicalURLPath to further cleanup the path.
|
||||
if prefix != "" && prefix[0] != '/' {
|
||||
prefix = "/" + prefix
|
||||
}
|
||||
if key != "" && key[0] != '/' {
|
||||
key = "/" + key
|
||||
}
|
||||
ep.Path = pathutil.CanonicalURLPath(ep.Path + prefix + key)
|
||||
return &ep
|
||||
}
|
||||
|
||||
type getAction struct {
|
||||
Prefix string
|
||||
Key string
|
||||
Recursive bool
|
||||
Sorted bool
|
||||
Quorum bool
|
||||
}
|
||||
|
||||
func (g *getAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2KeysURL(ep, g.Prefix, g.Key)
|
||||
|
||||
params := u.Query()
|
||||
params.Set("recursive", strconv.FormatBool(g.Recursive))
|
||||
params.Set("sorted", strconv.FormatBool(g.Sorted))
|
||||
params.Set("quorum", strconv.FormatBool(g.Quorum))
|
||||
u.RawQuery = params.Encode()
|
||||
|
||||
req, _ := http.NewRequest("GET", u.String(), nil)
|
||||
return req
|
||||
}
|
||||
|
||||
type waitAction struct {
|
||||
Prefix string
|
||||
Key string
|
||||
WaitIndex uint64
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
func (w *waitAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2KeysURL(ep, w.Prefix, w.Key)
|
||||
|
||||
params := u.Query()
|
||||
params.Set("wait", "true")
|
||||
params.Set("waitIndex", strconv.FormatUint(w.WaitIndex, 10))
|
||||
params.Set("recursive", strconv.FormatBool(w.Recursive))
|
||||
u.RawQuery = params.Encode()
|
||||
|
||||
req, _ := http.NewRequest("GET", u.String(), nil)
|
||||
return req
|
||||
}
|
||||
|
||||
type setAction struct {
|
||||
Prefix string
|
||||
Key string
|
||||
Value string
|
||||
PrevValue string
|
||||
PrevIndex uint64
|
||||
PrevExist PrevExistType
|
||||
TTL time.Duration
|
||||
Refresh bool
|
||||
Dir bool
|
||||
NoValueOnSuccess bool
|
||||
}
|
||||
|
||||
func (a *setAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2KeysURL(ep, a.Prefix, a.Key)
|
||||
|
||||
params := u.Query()
|
||||
form := url.Values{}
|
||||
|
||||
// we're either creating a directory or setting a key
|
||||
if a.Dir {
|
||||
params.Set("dir", strconv.FormatBool(a.Dir))
|
||||
} else {
|
||||
// These options are only valid for setting a key
|
||||
if a.PrevValue != "" {
|
||||
params.Set("prevValue", a.PrevValue)
|
||||
}
|
||||
form.Add("value", a.Value)
|
||||
}
|
||||
|
||||
// Options which apply to both setting a key and creating a dir
|
||||
if a.PrevIndex != 0 {
|
||||
params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
|
||||
}
|
||||
if a.PrevExist != PrevIgnore {
|
||||
params.Set("prevExist", string(a.PrevExist))
|
||||
}
|
||||
if a.TTL > 0 {
|
||||
form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
|
||||
}
|
||||
|
||||
if a.Refresh {
|
||||
form.Add("refresh", "true")
|
||||
}
|
||||
if a.NoValueOnSuccess {
|
||||
params.Set("noValueOnSuccess", strconv.FormatBool(a.NoValueOnSuccess))
|
||||
}
|
||||
|
||||
u.RawQuery = params.Encode()
|
||||
body := strings.NewReader(form.Encode())
|
||||
|
||||
req, _ := http.NewRequest("PUT", u.String(), body)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
type deleteAction struct {
|
||||
Prefix string
|
||||
Key string
|
||||
PrevValue string
|
||||
PrevIndex uint64
|
||||
Dir bool
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
func (a *deleteAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2KeysURL(ep, a.Prefix, a.Key)
|
||||
|
||||
params := u.Query()
|
||||
if a.PrevValue != "" {
|
||||
params.Set("prevValue", a.PrevValue)
|
||||
}
|
||||
if a.PrevIndex != 0 {
|
||||
params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
|
||||
}
|
||||
if a.Dir {
|
||||
params.Set("dir", "true")
|
||||
}
|
||||
if a.Recursive {
|
||||
params.Set("recursive", "true")
|
||||
}
|
||||
u.RawQuery = params.Encode()
|
||||
|
||||
req, _ := http.NewRequest("DELETE", u.String(), nil)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
type createInOrderAction struct {
|
||||
Prefix string
|
||||
Dir string
|
||||
Value string
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
func (a *createInOrderAction) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2KeysURL(ep, a.Prefix, a.Dir)
|
||||
|
||||
form := url.Values{}
|
||||
form.Add("value", a.Value)
|
||||
if a.TTL > 0 {
|
||||
form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
|
||||
}
|
||||
body := strings.NewReader(form.Encode())
|
||||
|
||||
req, _ := http.NewRequest("POST", u.String(), body)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
return req
|
||||
}
|
||||
|
||||
func unmarshalHTTPResponse(code int, header http.Header, body []byte) (res *Response, err error) {
|
||||
switch code {
|
||||
case http.StatusOK, http.StatusCreated:
|
||||
if len(body) == 0 {
|
||||
return nil, ErrEmptyBody
|
||||
}
|
||||
res, err = unmarshalSuccessfulKeysResponse(header, body)
|
||||
default:
|
||||
err = unmarshalFailedKeysResponse(body)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func unmarshalSuccessfulKeysResponse(header http.Header, body []byte) (*Response, error) {
|
||||
var res Response
|
||||
err := codec.NewDecoderBytes(body, new(codec.JsonHandle)).Decode(&res)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidJSON
|
||||
}
|
||||
if header.Get("X-Etcd-Index") != "" {
|
||||
res.Index, err = strconv.ParseUint(header.Get("X-Etcd-Index"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
res.ClusterID = header.Get("X-Etcd-Cluster-ID")
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func unmarshalFailedKeysResponse(body []byte) error {
|
||||
var etcdErr Error
|
||||
if err := json.Unmarshal(body, &etcdErr); err != nil {
|
||||
return ErrInvalidJSON
|
||||
}
|
||||
return etcdErr
|
||||
}
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
// Copyright 2015 The etcd Authors
|
||||
//
|
||||
// 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 client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultV2MembersPrefix = "/v2/members"
|
||||
defaultLeaderSuffix = "/leader"
|
||||
)
|
||||
|
||||
type Member struct {
|
||||
// ID is the unique identifier of this Member.
|
||||
ID string `json:"id"`
|
||||
|
||||
// Name is a human-readable, non-unique identifier of this Member.
|
||||
Name string `json:"name"`
|
||||
|
||||
// PeerURLs represents the HTTP(S) endpoints this Member uses to
|
||||
// participate in etcd's consensus protocol.
|
||||
PeerURLs []string `json:"peerURLs"`
|
||||
|
||||
// ClientURLs represents the HTTP(S) endpoints on which this Member
|
||||
// serves it's client-facing APIs.
|
||||
ClientURLs []string `json:"clientURLs"`
|
||||
}
|
||||
|
||||
type memberCollection []Member
|
||||
|
||||
func (c *memberCollection) UnmarshalJSON(data []byte) error {
|
||||
d := struct {
|
||||
Members []Member
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.Members == nil {
|
||||
*c = make([]Member, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
*c = d.Members
|
||||
return nil
|
||||
}
|
||||
|
||||
type memberCreateOrUpdateRequest struct {
|
||||
PeerURLs types.URLs
|
||||
}
|
||||
|
||||
func (m *memberCreateOrUpdateRequest) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
PeerURLs []string `json:"peerURLs"`
|
||||
}{
|
||||
PeerURLs: make([]string, len(m.PeerURLs)),
|
||||
}
|
||||
|
||||
for i, u := range m.PeerURLs {
|
||||
s.PeerURLs[i] = u.String()
|
||||
}
|
||||
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
// NewMembersAPI constructs a new MembersAPI that uses HTTP to
|
||||
// interact with etcd's membership API.
|
||||
func NewMembersAPI(c Client) MembersAPI {
|
||||
return &httpMembersAPI{
|
||||
client: c,
|
||||
}
|
||||
}
|
||||
|
||||
type MembersAPI interface {
|
||||
// List enumerates the current cluster membership.
|
||||
List(ctx context.Context) ([]Member, error)
|
||||
|
||||
// Add instructs etcd to accept a new Member into the cluster.
|
||||
Add(ctx context.Context, peerURL string) (*Member, error)
|
||||
|
||||
// Remove demotes an existing Member out of the cluster.
|
||||
Remove(ctx context.Context, mID string) error
|
||||
|
||||
// Update instructs etcd to update an existing Member in the cluster.
|
||||
Update(ctx context.Context, mID string, peerURLs []string) error
|
||||
|
||||
// Leader gets current leader of the cluster
|
||||
Leader(ctx context.Context) (*Member, error)
|
||||
}
|
||||
|
||||
type httpMembersAPI struct {
|
||||
client httpClient
|
||||
}
|
||||
|
||||
func (m *httpMembersAPI) List(ctx context.Context) ([]Member, error) {
|
||||
req := &membersAPIActionList{}
|
||||
resp, body, err := m.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var mCollection memberCollection
|
||||
if err := json.Unmarshal(body, &mCollection); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []Member(mCollection), nil
|
||||
}
|
||||
|
||||
func (m *httpMembersAPI) Add(ctx context.Context, peerURL string) (*Member, error) {
|
||||
urls, err := types.NewURLs([]string{peerURL})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &membersAPIActionAdd{peerURLs: urls}
|
||||
resp, body, err := m.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := assertStatusCode(resp.StatusCode, http.StatusCreated, http.StatusConflict); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
var merr membersError
|
||||
if err := json.Unmarshal(body, &merr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, merr
|
||||
}
|
||||
|
||||
var memb Member
|
||||
if err := json.Unmarshal(body, &memb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &memb, nil
|
||||
}
|
||||
|
||||
func (m *httpMembersAPI) Update(ctx context.Context, memberID string, peerURLs []string) error {
|
||||
urls, err := types.NewURLs(peerURLs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &membersAPIActionUpdate{peerURLs: urls, memberID: memberID}
|
||||
resp, body, err := m.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusNotFound, http.StatusConflict); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
var merr membersError
|
||||
if err := json.Unmarshal(body, &merr); err != nil {
|
||||
return err
|
||||
}
|
||||
return merr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *httpMembersAPI) Remove(ctx context.Context, memberID string) error {
|
||||
req := &membersAPIActionRemove{memberID: memberID}
|
||||
resp, _, err := m.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return assertStatusCode(resp.StatusCode, http.StatusNoContent, http.StatusGone)
|
||||
}
|
||||
|
||||
func (m *httpMembersAPI) Leader(ctx context.Context) (*Member, error) {
|
||||
req := &membersAPIActionLeader{}
|
||||
resp, body, err := m.client.Do(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var leader Member
|
||||
if err := json.Unmarshal(body, &leader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &leader, nil
|
||||
}
|
||||
|
||||
type membersAPIActionList struct{}
|
||||
|
||||
func (l *membersAPIActionList) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2MembersURL(ep)
|
||||
req, _ := http.NewRequest("GET", u.String(), nil)
|
||||
return req
|
||||
}
|
||||
|
||||
type membersAPIActionRemove struct {
|
||||
memberID string
|
||||
}
|
||||
|
||||
func (d *membersAPIActionRemove) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2MembersURL(ep)
|
||||
u.Path = path.Join(u.Path, d.memberID)
|
||||
req, _ := http.NewRequest("DELETE", u.String(), nil)
|
||||
return req
|
||||
}
|
||||
|
||||
type membersAPIActionAdd struct {
|
||||
peerURLs types.URLs
|
||||
}
|
||||
|
||||
func (a *membersAPIActionAdd) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2MembersURL(ep)
|
||||
m := memberCreateOrUpdateRequest{PeerURLs: a.peerURLs}
|
||||
b, _ := json.Marshal(&m)
|
||||
req, _ := http.NewRequest("POST", u.String(), bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
type membersAPIActionUpdate struct {
|
||||
memberID string
|
||||
peerURLs types.URLs
|
||||
}
|
||||
|
||||
func (a *membersAPIActionUpdate) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2MembersURL(ep)
|
||||
m := memberCreateOrUpdateRequest{PeerURLs: a.peerURLs}
|
||||
u.Path = path.Join(u.Path, a.memberID)
|
||||
b, _ := json.Marshal(&m)
|
||||
req, _ := http.NewRequest("PUT", u.String(), bytes.NewReader(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
}
|
||||
|
||||
func assertStatusCode(got int, want ...int) (err error) {
|
||||
for _, w := range want {
|
||||
if w == got {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("unexpected status code %d", got)
|
||||
}
|
||||
|
||||
type membersAPIActionLeader struct{}
|
||||
|
||||
func (l *membersAPIActionLeader) HTTPRequest(ep url.URL) *http.Request {
|
||||
u := v2MembersURL(ep)
|
||||
u.Path = path.Join(u.Path, defaultLeaderSuffix)
|
||||
req, _ := http.NewRequest("GET", u.String(), nil)
|
||||
return req
|
||||
}
|
||||
|
||||
// v2MembersURL add the necessary path to the provided endpoint
|
||||
// to route requests to the default v2 members API.
|
||||
func v2MembersURL(ep url.URL) *url.URL {
|
||||
ep.Path = path.Join(ep.Path, defaultV2MembersPrefix)
|
||||
return &ep
|
||||
}
|
||||
|
||||
type membersError struct {
|
||||
Message string `json:"message"`
|
||||
Code int `json:"-"`
|
||||
}
|
||||
|
||||
func (e membersError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user