From 7923b83953fae7e5e8b910d88767f0e03211f126 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Fri, 22 Feb 2019 19:18:01 -0800 Subject: [PATCH] Support multiple-domains in MINIO_DOMAIN (#7274) Fixes #7173 --- cmd/api-response.go | 5 +- cmd/api-response_test.go | 8 ++-- cmd/api-router.go | 4 +- cmd/bucket-handlers.go | 6 +-- cmd/common-main.go | 18 ++++--- cmd/config-current_test.go | 4 +- cmd/generic-handlers.go | 4 +- cmd/globals.go | 7 ++- cmd/handler-utils.go | 15 +++--- cmd/handler-utils_test.go | 10 ++-- cmd/object-handlers.go | 2 +- cmd/signature-v2.go | 4 +- docs/config/README.md | 6 +++ pkg/dns/etcd_dns.go | 98 +++++++++++++++++++++++--------------- 14 files changed, 113 insertions(+), 78 deletions(-) diff --git a/cmd/api-response.go b/cmd/api-response.go index 57a975c31..49522150a 100644 --- a/cmd/api-response.go +++ b/cmd/api-response.go @@ -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() diff --git a/cmd/api-response_test.go b/cmd/api-response_test.go index bde1b1828..c69220791 100644 --- a/cmd/api-response_test.go +++ b/cmd/api-response_test.go @@ -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) } diff --git a/cmd/api-router.go b/cmd/api-router.go index 1de35e97d..5e8671f86 100644 --- a/cmd/api-router.go +++ b/cmd/api-router.go @@ -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()) diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go index a5ddeff4b..a66047d4e 100644 --- a/cmd/bucket-handlers.go +++ b/cmd/bucket-handlers.go @@ -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) diff --git a/cmd/common-main.go b/cmd/common-main.go index 68cadae17..beeacd809 100644 --- a/cmd/common-main.go +++ b/cmd/common-main.go @@ -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 != "" { diff --git a/cmd/config-current_test.go b/cmd/config-current_test.go index bcd32709f..d70258905 100644 --- a/cmd/config-current_test.go +++ b/cmd/config-current_test.go @@ -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]) } } diff --git a/cmd/generic-handlers.go b/cmd/generic-handlers.go index 0a44c30eb..fb73bdae9 100644 --- a/cmd/generic-handlers.go +++ b/cmd/generic-handlers.go @@ -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: diff --git a/cmd/globals.go b/cmd/globals.go index f3b6a780e..37afb39bf 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -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 diff --git a/cmd/handler-utils.go b/cmd/handler-utils.go index 1fc5220a4..3379aae80 100644 --- a/cmd/handler-utils.go +++ b/cmd/handler-utils.go @@ -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 diff --git a/cmd/handler-utils_test.go b/cmd/handler-utils_test.go index c83daf201..a2985c360 100644 --- a/cmd/handler-utils_test.go +++ b/cmd/handler-utils_test.go @@ -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) } diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go index 44c360d64..49b5a1741 100644 --- a/cmd/object-handlers.go +++ b/cmd/object-handlers.go @@ -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 diff --git a/cmd/signature-v2.go b/cmd/signature-v2.go index 39ebb6ba1..d9d6e94fd 100644 --- a/cmd/signature-v2.go +++ b/cmd/signature-v2.go @@ -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 } diff --git a/docs/config/README.md b/docs/config/README.md index 28cb9a533..57e252226 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -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. diff --git a/pkg/dns/etcd_dns.go b/pkg/dns/etcd_dns.go index 216208714..911897366 100644 --- a/pkg/dns/etcd_dns.go +++ b/pkg/dns/etcd_dns.go @@ -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 }