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. // 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. // unit tests do not have host set.
if r.Host == "" { if r.Host == "" {
return path.Clean(r.URL.Path) return path.Clean(r.URL.Path)
@ -295,10 +295,11 @@ func getObjectLocation(r *http.Request, domain, bucket, object string) string {
Scheme: proto, Scheme: proto,
} }
// If domain is set then we need to use bucket DNS style. // If domain is set then we need to use bucket DNS style.
if domain != "" { for _, domain := range domains {
if strings.Contains(r.Host, domain) { if strings.Contains(r.Host, domain) {
u.Host = bucket + "." + r.Host u.Host = bucket + "." + r.Host
u.Path = path.Join(slashSeparator, object) u.Path = path.Join(slashSeparator, object)
break
} }
} }
return u.String() return u.String()

View File

@ -26,7 +26,7 @@ func TestObjectLocation(t *testing.T) {
testCases := []struct { testCases := []struct {
request *http.Request request *http.Request
bucket, object string bucket, object string
domain string domains []string
expectedLocation string expectedLocation string
}{ }{
// Server binding to localhost IP with https. // Server binding to localhost IP with https.
@ -80,7 +80,7 @@ func TestObjectLocation(t *testing.T) {
Host: "mys3.bucket.org", Host: "mys3.bucket.org",
Header: map[string][]string{}, Header: map[string][]string{},
}, },
domain: "mys3.bucket.org", domains: []string{"mys3.bucket.org"},
bucket: "mybucket", bucket: "mybucket",
object: "test/1.txt", object: "test/1.txt",
expectedLocation: "http://mybucket.mys3.bucket.org/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}, "X-Forwarded-Scheme": {httpsScheme},
}, },
}, },
domain: "mys3.bucket.org", domains: []string{"mys3.bucket.org"},
bucket: "mybucket", bucket: "mybucket",
object: "test/1.txt", object: "test/1.txt",
expectedLocation: "https://mybucket.mys3.bucket.org/test/1.txt", expectedLocation: "https://mybucket.mys3.bucket.org/test/1.txt",
}, },
} }
for i, testCase := range testCases { 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 { if testCase.expectedLocation != gotLocation {
t.Errorf("Test %d: expected %s, got %s", i+1, 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 // API Router
apiRouter := router.PathPrefix("/").Subrouter() apiRouter := router.PathPrefix("/").Subrouter()
var routers []*mux.Router var routers []*mux.Router
if globalDomainName != "" { for _, domainName := range globalDomainNames {
routers = append(routers, apiRouter.Host("{bucket:.+}."+globalDomainName).Subrouter()) routers = append(routers, apiRouter.Host("{bucket:.+}."+domainName).Subrouter())
} }
routers = append(routers, apiRouter.PathPrefix("/{bucket}").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 // 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) writeSuccessResponseHeadersOnly(w)
return return
@ -505,7 +505,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL, guessIsBrowserReq(r))
return return
} }
resource, err := getResource(r.URL.Path, r.Host, globalDomainName) resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r)) writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
return return
@ -677,7 +677,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
return return
} }
location := getObjectLocation(r, globalDomainName, bucket, object) location := getObjectLocation(r, globalDomainNames, bucket, object)
w.Header().Set("ETag", `"`+objInfo.ETag+`"`) w.Header().Set("ETag", `"`+objInfo.ETag+`"`)
w.Header().Set("Location", location) w.Header().Set("Location", location)

View File

@ -262,10 +262,14 @@ func handleCommonEnvVars() {
logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints) logger.FatalIf(err, "Unable to initialize etcd with %s", etcdEndpoints)
} }
globalDomainName, globalIsEnvDomainName = os.LookupEnv("MINIO_DOMAIN") v, ok := os.LookupEnv("MINIO_DOMAIN")
if globalDomainName != "" { if ok {
if _, ok = dns2.IsDomainName(globalDomainName); !ok { for _, domainName := range strings.Split(v, ",") {
logger.Fatal(uiErrInvalidDomainValue(nil).Msg("Unknown value `%s`", globalDomainName), "Invalid MINIO_DOMAIN value in environment variable") 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) updateDomainIPs(localIP4)
} }
if globalDomainName != "" && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil { if len(globalDomainNames) != 0 && !globalDomainIPs.IsEmpty() && globalEtcdClient != nil {
var err error var err error
globalDNSConfig, err = dns.NewCoreDNS(globalDomainName, globalDomainIPs, globalMinioPort, globalEtcdClient) globalDNSConfig, err = dns.NewCoreDNS(globalDomainNames, globalDomainIPs, globalMinioPort, globalEtcdClient)
logger.FatalIf(err, "Unable to initialize DNS config for %s.", globalDomainName) logger.FatalIf(err, "Unable to initialize DNS config for %s.", globalDomainNames)
} }
if drives := os.Getenv("MINIO_CACHE_DRIVES"); drives != "" { 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 // Check if serverConfig has the correct domain
if globalDomainName != "domain.com" { if globalDomainNames[0] != "domain.com" {
t.Errorf("Expected Domain to be `domain.com`, found " + globalDomainName) 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) { func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if globalDNSConfig == nil || globalDomainName == "" || if globalDNSConfig == nil || len(globalDomainNames) == 0 ||
guessIsHealthCheckReq(r) || guessIsMetricsReq(r) || guessIsHealthCheckReq(r) || guessIsMetricsReq(r) ||
guessIsRPCReq(r) || isAdminReq(r) { guessIsRPCReq(r) || isAdminReq(r) {
f.handler.ServeHTTP(w, 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 // For browser requests, when federation is setup we need to
// specifically handle download and upload for browser requests. // 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 var bucket, _ string
switch r.Method { switch r.Method {
case http.MethodPut: case http.MethodPut:

View File

@ -33,7 +33,7 @@ import (
"github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/auth"
"github.com/minio/minio/pkg/certs" "github.com/minio/minio/pkg/certs"
"github.com/minio/minio/pkg/dns" "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" "github.com/minio/minio/pkg/iam/validator"
) )
@ -175,9 +175,8 @@ var (
globalActiveCred auth.Credentials globalActiveCred auth.Credentials
globalPublicCerts []*x509.Certificate globalPublicCerts []*x509.Certificate
globalIsEnvDomainName bool globalDomainNames []string // Root domains 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
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 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 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. // Returns "/bucketName/objectName" for path-style or virtual-host-style requests.
func getResource(path string, host string, domain string) (string, error) { func getResource(path string, host string, domains []string) (string, error) {
if domain == "" { if len(domains) == 0 {
return path, nil return path, nil
} }
// If virtual-host-style is enabled construct the "resource" properly. // 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 return "", err
} }
} }
if !strings.HasSuffix(host, "."+domain) { for _, domain := range domains {
return path, nil if !strings.HasSuffix(host, "."+domain) {
continue
}
bucket := strings.TrimSuffix(host, "."+domain)
return slashSeparator + pathJoin(bucket, path), nil
} }
bucket := strings.TrimSuffix(host, "."+domain) return path, nil
return slashSeparator + pathJoin(bucket, path), nil
} }
// If none of the http routes match respond with MethodNotAllowed, in JSON // 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 { testCases := []struct {
p string p string
host string host string
domain string domains []string
expectedResource string expectedResource string
}{ }{
{"/a/b/c", "test.mydomain.com", "mydomain.com", "/test/a/b/c"}, {"/a/b/c", "test.mydomain.com", []string{"mydomain.com"}, "/test/a/b/c"},
{"/a/b/c", "test.mydomain.com", "notmydomain.com", "/a/b/c"}, {"/a/b/c", "test.mydomain.com", []string{"notmydomain.com"}, "/a/b/c"},
{"/a/b/c", "test.mydomain.com", "", "/a/b/c"}, {"/a/b/c", "test.mydomain.com", nil, "/a/b/c"},
} }
for i, test := range testCases { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

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

View File

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

View File

@ -156,6 +156,12 @@ export MINIO_DOMAIN=mydomain.com
minio server /data 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 ### 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. 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. // Retrieves list of DNS entries for the domain.
func (c *coreDNS) List() ([]SrvRecord, error) { func (c *coreDNS) List() ([]SrvRecord, error) {
key := msg.Path(fmt.Sprintf("%s.", c.domainName), defaultPrefixPath) var srvRecords []SrvRecord
return c.list(key) 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. // Retrieves DNS records for a bucket.
func (c *coreDNS) Get(bucket string) ([]SrvRecord, error) { func (c *coreDNS) Get(bucket string) ([]SrvRecord, error) {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, c.domainName), defaultPrefixPath) var srvRecords []SrvRecord
return c.list(key) 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. // 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, // In all other situations when we want to list all DNS records,
// which is handled in the else clause. // which is handled in the else clause.
if key != msg.Path(fmt.Sprintf(".%s.", c.domainName), defaultPrefixPath) { for _, domainName := range c.domainNames {
if srvRecord.Key == "/" { if key != msg.Path(fmt.Sprintf(".%s.", domainName), defaultPrefixPath) {
if srvRecord.Key == "/" {
srvRecords = append(srvRecords, srvRecord)
}
} else {
srvRecords = append(srvRecords, srvRecord) srvRecords = append(srvRecords, srvRecord)
} }
} else {
srvRecords = append(srvRecords, srvRecord)
} }
} }
@ -117,16 +135,18 @@ func (c *coreDNS) Put(bucket string) error {
if err != nil { if err != nil {
return err return err
} }
key := msg.Path(fmt.Sprintf("%s.%s", bucket, c.domainName), defaultPrefixPath) for _, domainName := range c.domainNames {
key = key + "/" + ip key := msg.Path(fmt.Sprintf("%s.%s", bucket, domainName), defaultPrefixPath)
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) key = key + "/" + ip
_, err = c.etcdClient.Put(ctx, key, string(bucketMsg)) ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
defer cancel() _, err = c.etcdClient.Put(ctx, key, string(bucketMsg))
if err != nil {
ctx, cancel = context.WithTimeout(context.Background(), defaultContextTimeout)
c.etcdClient.Delete(ctx, key)
defer cancel() 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 return nil
@ -134,33 +154,35 @@ func (c *coreDNS) Put(bucket string) error {
// Removes DNS entries added in Put(). // Removes DNS entries added in Put().
func (c *coreDNS) Delete(bucket string) error { func (c *coreDNS) Delete(bucket string) error {
key := msg.Path(fmt.Sprintf("%s.%s.", bucket, c.domainName), defaultPrefixPath) for _, domainName := range c.domainNames {
srvRecords, err := c.list(key) key := msg.Path(fmt.Sprintf("%s.%s.", bucket, domainName), defaultPrefixPath)
if err != nil { srvRecords, err := c.list(key)
return err if err != nil {
}
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 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. // CoreDNS - represents dns config for coredns server.
type coreDNS struct { type coreDNS struct {
domainName string domainNames []string
domainIPs set.StringSet domainIPs set.StringSet
domainPort int domainPort int
etcdClient *etcd.Client etcdClient *etcd.Client
} }
// NewCoreDNS - initialize a new coreDNS set/unset values. // NewCoreDNS - initialize a new coreDNS set/unset values.
func NewCoreDNS(domainName string, domainIPs set.StringSet, domainPort string, etcdClient *etcd.Client) (Config, error) { func NewCoreDNS(domainNames []string, domainIPs set.StringSet, domainPort string, etcdClient *etcd.Client) (Config, error) {
if domainName == "" || domainIPs.IsEmpty() { if len(domainNames) == 0 || domainIPs.IsEmpty() {
return nil, errors.New("invalid argument") return nil, errors.New("invalid argument")
} }
@ -170,9 +192,9 @@ func NewCoreDNS(domainName string, domainIPs set.StringSet, domainPort string, e
} }
return &coreDNS{ return &coreDNS{
domainName: domainName, domainNames: domainNames,
domainIPs: domainIPs, domainIPs: domainIPs,
domainPort: port, domainPort: port,
etcdClient: etcdClient, etcdClient: etcdClient,
}, nil }, nil
} }