Support multiple-domains in MINIO_DOMAIN (#7274)

Fixes #7173
This commit is contained in:
Harshavardhana 2019-02-22 19:18:01 -08:00 committed by Nitish Tiwari
parent 80a351633f
commit 7923b83953
14 changed files with 113 additions and 78 deletions

View File

@ -280,7 +280,7 @@ func getURLScheme(tls bool) string {
}
// getObjectLocation gets the fully qualified URL of an object.
func getObjectLocation(r *http.Request, domain, bucket, object string) string {
func getObjectLocation(r *http.Request, domains []string, bucket, object string) string {
// unit tests do not have host set.
if r.Host == "" {
return path.Clean(r.URL.Path)
@ -295,10 +295,11 @@ func getObjectLocation(r *http.Request, domain, bucket, object string) string {
Scheme: proto,
}
// If domain is set then we need to use bucket DNS style.
if domain != "" {
for _, domain := range domains {
if strings.Contains(r.Host, domain) {
u.Host = bucket + "." + r.Host
u.Path = path.Join(slashSeparator, object)
break
}
}
return u.String()

View File

@ -26,7 +26,7 @@ func TestObjectLocation(t *testing.T) {
testCases := []struct {
request *http.Request
bucket, object string
domain string
domains []string
expectedLocation string
}{
// Server binding to localhost IP with https.
@ -80,7 +80,7 @@ func TestObjectLocation(t *testing.T) {
Host: "mys3.bucket.org",
Header: map[string][]string{},
},
domain: "mys3.bucket.org",
domains: []string{"mys3.bucket.org"},
bucket: "mybucket",
object: "test/1.txt",
expectedLocation: "http://mybucket.mys3.bucket.org/test/1.txt",
@ -92,14 +92,14 @@ func TestObjectLocation(t *testing.T) {
"X-Forwarded-Scheme": {httpsScheme},
},
},
domain: "mys3.bucket.org",
domains: []string{"mys3.bucket.org"},
bucket: "mybucket",
object: "test/1.txt",
expectedLocation: "https://mybucket.mys3.bucket.org/test/1.txt",
},
}
for i, testCase := range testCases {
gotLocation := getObjectLocation(testCase.request, testCase.domain, testCase.bucket, testCase.object)
gotLocation := getObjectLocation(testCase.request, testCase.domains, testCase.bucket, testCase.object)
if testCase.expectedLocation != gotLocation {
t.Errorf("Test %d: expected %s, got %s", i+1, testCase.expectedLocation, gotLocation)
}

View File

@ -44,8 +44,8 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled bool) {
// API Router
apiRouter := router.PathPrefix("/").Subrouter()
var routers []*mux.Router
if globalDomainName != "" {
routers = append(routers, apiRouter.Host("{bucket:.+}."+globalDomainName).Subrouter())
for _, domainName := range globalDomainNames {
routers = append(routers, apiRouter.Host("{bucket:.+}."+domainName).Subrouter())
}
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())

View File

@ -446,7 +446,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
}
// Make sure to add Location information here only for bucket
w.Header().Set("Location", getObjectLocation(r, globalDomainName, bucket, ""))
w.Header().Set("Location", getObjectLocation(r, globalDomainNames, bucket, ""))
writeSuccessResponseHeadersOnly(w)
return
@ -505,7 +505,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r))
return
}
resource, err := getResource(r.URL.Path, r.Host, globalDomainName)
resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
return
@ -677,7 +677,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return
}
location := getObjectLocation(r, globalDomainName, bucket, object)
location := getObjectLocation(r, globalDomainNames, bucket, object)
w.Header().Set("ETag", `"`+objInfo.ETag+`"`)
w.Header().Set("Location", location)

View File

@ -262,10 +262,14 @@ func handleCommonEnvVars() {
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
}
globalDomainName, globalIsEnvDomainName = os.LookupEnv("MINIO_DOMAIN")
if globalDomainName != "" {
if _, ok = dns2.IsDomainName(globalDomainName); !ok {
logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", globalDomainName), "Invalid MINIO_DOMAIN value in environment variable")
v, ok := os.LookupEnv("MINIO_DOMAIN")
if ok {
for _, domainName := range strings.Split(v, ",") {
if _, ok = dns2.IsDomainName(domainName); !ok {
logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", domainName),
"Invalid MINIO_DOMAIN value in environment variable")
}
globalDomainNames = append(globalDomainNames, domainName)
}
}
@ -294,10 +298,10 @@ func handleCommonEnvVars() {
updateDomainIPs(localIP4)
}
if globalDomainName != "" && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
if len(globalDomainNames) != 0 && !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)
globalDNSConfig, err = dns.NewCoreDNS(globalDomainNames, globalDomainIPs, globalMinioPort, globalEtcdClient)
logger.FatalIf(err, "Unable to initialize DNS config for %s.", globalDomainNames)
}
if drives := os.Getenv("MINIO_CACHE_DRIVES"); drives != "" {

View File

@ -130,8 +130,8 @@ func TestServerConfigWithEnvs(t *testing.T) {
}
// Check if serverConfig has the correct domain
if globalDomainName != "domain.com" {
t.Errorf("Expected Domain to be `domain.com`, found " + globalDomainName)
if globalDomainNames[0] != "domain.com" {
t.Errorf("Expected Domain to be `domain.com`, found " + globalDomainNames[0])
}
}

View File

@ -623,7 +623,7 @@ type bucketForwardingHandler struct {
}
func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if globalDNSConfig == nil || globalDomainName == "" ||
if globalDNSConfig == nil || len(globalDomainNames) == 0 ||
guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
guessIsRPCReq(r) || isAdminReq(r) {
f.handler.ServeHTTP(w, r)
@ -632,7 +632,7 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
// For browser requests, when federation is setup we need to
// specifically handle download and upload for browser requests.
if guessIsBrowserReq(r) && globalDNSConfig != nil && globalDomainName != "" {
if guessIsBrowserReq(r) && globalDNSConfig != nil && len(globalDomainNames) > 0 {
var bucket, _ string
switch r.Method {
case http.MethodPut:

View File

@ -33,7 +33,7 @@ import (
"github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/certs"
"github.com/minio/minio/pkg/dns"
"github.com/minio/minio/pkg/iam/policy"
iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/iam/validator"
)
@ -175,9 +175,8 @@ var (
globalActiveCred auth.Credentials
globalPublicCerts []*x509.Certificate
globalIsEnvDomainName bool
globalDomainName string // Root domain for virtual host style requests
globalDomainIPs set.StringSet // Root domain IP address(s) for a distributed Minio deployment
globalDomainNames []string // Root domains 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

View File

@ -341,8 +341,8 @@ func httpTraceHdrs(f http.HandlerFunc) http.HandlerFunc {
}
// Returns "/bucketName/objectName" for path-style or virtual-host-style requests.
func getResource(path string, host string, domain string) (string, error) {
if domain == "" {
func getResource(path string, host string, domains []string) (string, error) {
if len(domains) == 0 {
return path, nil
}
// If virtual-host-style is enabled construct the "resource" properly.
@ -357,11 +357,14 @@ func getResource(path string, host string, domain string) (string, error) {
return "", err
}
}
if !strings.HasSuffix(host, "."+domain) {
return path, nil
for _, domain := range domains {
if !strings.HasSuffix(host, "."+domain) {
continue
}
bucket := strings.TrimSuffix(host, "."+domain)
return slashSeparator + pathJoin(bucket, path), nil
}
bucket := strings.TrimSuffix(host, "."+domain)
return slashSeparator + pathJoin(bucket, path), nil
return path, nil
}
// If none of the http routes match respond with MethodNotAllowed, in JSON

View File

@ -213,15 +213,15 @@ func TestGetResource(t *testing.T) {
testCases := []struct {
p string
host string
domain string
domains []string
expectedResource string
}{
{"/a/b/c", "test.mydomain.com", "mydomain.com", "/test/a/b/c"},
{"/a/b/c", "test.mydomain.com", "notmydomain.com", "/a/b/c"},
{"/a/b/c", "test.mydomain.com", "", "/a/b/c"},
{"/a/b/c", "test.mydomain.com", []string{"mydomain.com"}, "/test/a/b/c"},
{"/a/b/c", "test.mydomain.com", []string{"notmydomain.com"}, "/a/b/c"},
{"/a/b/c", "test.mydomain.com", nil, "/a/b/c"},
}
for i, test := range testCases {
gotResource, err := getResource(test.p, test.host, test.domain)
gotResource, err := getResource(test.p, test.host, test.domains)
if err != nil {
t.Fatal(err)
}

View File

@ -2370,7 +2370,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
}
// Get object location.
location := getObjectLocation(r, globalDomainName, bucket, object)
location := getObjectLocation(r, globalDomainNames, bucket, object)
// Generate complete multipart response.
response := generateCompleteMultpartUploadResponse(bucket, object, location, objInfo.ETag)
var encodedSuccessResponse []byte

View File

@ -163,7 +163,7 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
return ErrExpiredPresignRequest
}
encodedResource, err = getResource(encodedResource, r.Host, globalDomainName)
encodedResource, err = getResource(encodedResource, r.Host, globalDomainNames)
if err != nil {
return ErrInvalidRequest
}
@ -257,7 +257,7 @@ func doesSignV2Match(r *http.Request) APIErrorCode {
return ErrInvalidQueryParams
}
encodedResource, err = getResource(encodedResource, r.Host, globalDomainName)
encodedResource, err = getResource(encodedResource, r.Host, globalDomainNames)
if err != nil {
return ErrInvalidRequest
}

View File

@ -156,6 +156,12 @@ export MINIO_DOMAIN=mydomain.com
minio server /data
```
For advanced use cases `MINIO_DOMAIN` environment variable supports multiple-domains with comma separated values.
```sh
export MINIO_DOMAIN=sub1.mydomain.com,sub2.mydomain.com
minio server /data
```
### Drive Sync
By default, Minio writes to disk in synchronous mode for all metadata operations. Set `MINIO_DRIVE_SYNC` environment variable to enable synchronous mode for all data operations as well.

View File

@ -47,14 +47,30 @@ func newCoreDNSMsg(bucket, ip string, port int, ttl uint32) ([]byte, error) {
// 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)
var srvRecords []SrvRecord
for _, domainName := range c.domainNames {
key := msg.Path(fmt.Sprintf("%s.", domainName), defaultPrefixPath)
records, err := c.list(key)
if err != nil {
return nil, err
}
srvRecords = append(srvRecords, records...)
}
return srvRecords, nil
}
// 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)
var srvRecords []SrvRecord
for _, domainName := range c.domainNames {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, domainName), defaultPrefixPath)
records, err := c.list(key)
if err != nil {
return nil, err
}
srvRecords = append(srvRecords, records...)
}
return srvRecords, nil
}
// Retrieves list of entries under the key passed.
@ -92,12 +108,14 @@ func (c *coreDNS) list(key string) ([]SrvRecord, error) {
//
// In all other situations when we want to list all DNS records,
// which is handled in the else clause.
if key != msg.Path(fmt.Sprintf(".%s.", c.domainName), defaultPrefixPath) {
if srvRecord.Key == "/" {
for _, domainName := range c.domainNames {
if key != msg.Path(fmt.Sprintf(".%s.", domainName), defaultPrefixPath) {
if srvRecord.Key == "/" {
srvRecords = append(srvRecords, srvRecord)
}
} else {
srvRecords = append(srvRecords, srvRecord)
}
} else {
srvRecords = append(srvRecords, srvRecord)
}
}
@ -117,16 +135,18 @@ func (c *coreDNS) Put(bucket string) error {
if err != nil {
return err
}
key := msg.Path(fmt.Sprintf("%s.%s", bucket, c.domainName), defaultPrefixPath)
key = key + "/" + ip
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
_, err = c.etcdClient.Put(ctx, key, string(bucketMsg))
defer cancel()
if err != nil {
ctx, cancel = context.WithTimeout(context.Background(), defaultContextTimeout)
c.etcdClient.Delete(ctx, key)
for _, domainName := range c.domainNames {
key := msg.Path(fmt.Sprintf("%s.%s", bucket, domainName), defaultPrefixPath)
key = key + "/" + ip
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
_, err = c.etcdClient.Put(ctx, key, string(bucketMsg))
defer cancel()
return err
if err != nil {
ctx, cancel = context.WithTimeout(context.Background(), defaultContextTimeout)
c.etcdClient.Delete(ctx, key)
defer cancel()
return err
}
}
}
return nil
@ -134,33 +154,35 @@ func (c *coreDNS) Put(bucket string) error {
// Removes DNS entries added in Put().
func (c *coreDNS) Delete(bucket string) error {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, c.domainName), defaultPrefixPath)
srvRecords, err := c.list(key)
if err != nil {
return err
}
for _, record := range srvRecords {
dctx, dcancel := context.WithTimeout(context.Background(), defaultContextTimeout)
if _, err = c.etcdClient.Delete(dctx, key+"/"+record.Host); err != nil {
dcancel()
for _, domainName := range c.domainNames {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, domainName), defaultPrefixPath)
srvRecords, err := c.list(key)
if err != nil {
return err
}
dcancel()
for _, record := range srvRecords {
dctx, dcancel := context.WithTimeout(context.Background(), defaultContextTimeout)
if _, err = c.etcdClient.Delete(dctx, key+"/"+record.Host); err != nil {
dcancel()
return err
}
dcancel()
}
}
return err
return nil
}
// CoreDNS - represents dns config for coredns server.
type coreDNS struct {
domainName string
domainIPs set.StringSet
domainPort int
etcdClient *etcd.Client
domainNames []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() {
func NewCoreDNS(domainNames []string, domainIPs set.StringSet, domainPort string, etcdClient *etcd.Client) (Config, error) {
if len(domainNames) == 0 || domainIPs.IsEmpty() {
return nil, errors.New("invalid argument")
}
@ -170,9 +192,9 @@ func NewCoreDNS(domainName string, domainIPs set.StringSet, domainPort string, e
}
return &coreDNS{
domainName: domainName,
domainIPs: domainIPs,
domainPort: port,
etcdClient: etcdClient,
domainNames: domainNames,
domainIPs: domainIPs,
domainPort: port,
etcdClient: etcdClient,
}, nil
}