common/peerprovider/ringpopprovider/config.go (227 lines of code) (raw):
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package ringpopprovider
import (
"context"
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/uber/ringpop-go/discovery"
"github.com/uber/ringpop-go/discovery/jsonfile"
"github.com/uber/ringpop-go/discovery/statichosts"
"github.com/uber/cadence/common/log"
"github.com/uber/cadence/common/log/tag"
)
// BootstrapMode is an enum type for ringpop bootstrap mode
type BootstrapMode int
const (
// BootstrapModeNone represents a bootstrap mode set to nothing or invalid
BootstrapModeNone BootstrapMode = iota
// BootstrapModeFile represents a file-based bootstrap mode
BootstrapModeFile
// BootstrapModeHosts represents a list of hosts passed in the configuration
BootstrapModeHosts
// BootstrapModeCustom represents a custom bootstrap mode
BootstrapModeCustom
// BootstrapModeDNS represents a list of hosts passed in the configuration
// to be resolved, and the resulting addresses are used for bootstrap
BootstrapModeDNS
// BootstrapModeDNSSRV represents a list of DNS hosts passed in the configuration
// to resolve secondary addresses that DNS SRV record would return resulting in
// a host list that will contain multiple dynamic addresses and their unique ports
BootstrapModeDNSSRV
)
const (
defaultMaxJoinDuration = 10 * time.Second
)
// Config contains the ringpop config items
type Config struct {
// Name to be used in ringpop advertisement
Name string `yaml:"name" validate:"nonzero"`
// BootstrapMode is a enum that defines the ringpop bootstrap method, currently supports: hosts, files, custom, dns, and dns-srv
BootstrapMode BootstrapMode `yaml:"bootstrapMode"`
// BootstrapHosts is a list of seed hosts to be used for ringpop bootstrap
BootstrapHosts []string `yaml:"bootstrapHosts"`
// BootstrapFile is the file path to be used for ringpop bootstrap
BootstrapFile string `yaml:"bootstrapFile"`
// MaxJoinDuration is the max wait time to join the ring
MaxJoinDuration time.Duration `yaml:"maxJoinDuration"`
// Custom discovery provider, cannot be specified through yaml
DiscoveryProvider discovery.DiscoverProvider `yaml:"-"`
}
func (rpConfig *Config) validate() error {
if len(rpConfig.Name) == 0 {
return fmt.Errorf("ringpop config missing `name` param")
}
if rpConfig.MaxJoinDuration == 0 {
rpConfig.MaxJoinDuration = defaultMaxJoinDuration
}
return validateBootstrapMode(rpConfig)
}
// UnmarshalYAML is called by the yaml package to convert
// the config YAML into a BootstrapMode.
func (m *BootstrapMode) UnmarshalYAML(
unmarshal func(interface{}) error,
) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
var err error
*m, err = parseBootstrapMode(s)
return err
}
// parseBootstrapMode reads a string value and returns a bootstrap mode.
func parseBootstrapMode(
mode string,
) (BootstrapMode, error) {
switch strings.ToLower(mode) {
case "hosts":
return BootstrapModeHosts, nil
case "file":
return BootstrapModeFile, nil
case "custom":
return BootstrapModeCustom, nil
case "dns":
return BootstrapModeDNS, nil
case "dns-srv":
return BootstrapModeDNSSRV, nil
}
return BootstrapModeNone, fmt.Errorf("invalid ringpop bootstrap mode %q", mode)
}
func validateBootstrapMode(
rpConfig *Config,
) error {
switch rpConfig.BootstrapMode {
case BootstrapModeFile:
if len(rpConfig.BootstrapFile) == 0 {
return fmt.Errorf("ringpop config missing bootstrap file param")
}
case BootstrapModeHosts, BootstrapModeDNS, BootstrapModeDNSSRV:
if len(rpConfig.BootstrapHosts) == 0 {
return fmt.Errorf("ringpop config missing boostrap hosts param")
}
case BootstrapModeCustom:
if rpConfig.DiscoveryProvider == nil {
return fmt.Errorf("ringpop bootstrapMode is set to custom but discoveryProvider is nil")
}
default:
return fmt.Errorf("ringpop config with unknown boostrap mode %q", rpConfig.BootstrapMode)
}
return nil
}
type dnsHostResolver interface {
LookupHost(ctx context.Context, host string) (addrs []string, err error)
LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error)
}
type dnsProvider struct {
UnresolvedHosts []string
Resolver dnsHostResolver
Logger log.Logger
}
func newDNSProvider(
hosts []string,
resolver dnsHostResolver,
logger log.Logger,
) *dnsProvider {
set := map[string]struct{}{}
for _, hostport := range hosts {
set[hostport] = struct{}{}
}
var keys []string
for key := range set {
keys = append(keys, key)
}
return &dnsProvider{
UnresolvedHosts: keys,
Resolver: resolver,
Logger: logger,
}
}
func (provider *dnsProvider) Hosts() ([]string, error) {
var results []string
resolvedHosts := map[string][]string{}
for _, hostPort := range provider.UnresolvedHosts {
host, port, err := net.SplitHostPort(hostPort)
if err != nil {
provider.Logger.Warn("could not split host and port", tag.Address(hostPort), tag.Error(err))
continue
}
resolved, exists := resolvedHosts[host]
if !exists {
resolved, err = provider.Resolver.LookupHost(context.Background(), host)
if err != nil {
provider.Logger.Warn("could not resolve host", tag.Address(host), tag.Error(err))
continue
}
resolvedHosts[host] = resolved
}
for _, r := range resolved {
results = append(results, net.JoinHostPort(r, port))
}
}
if len(results) == 0 {
return nil, errors.New("no hosts found, and bootstrap requires at least one")
}
return results, nil
}
type dnsSRVProvider struct {
UnresolvedHosts []string
Resolver dnsHostResolver
Logger log.Logger
}
func newDNSSRVProvider(
hosts []string,
resolver dnsHostResolver,
logger log.Logger,
) *dnsSRVProvider {
set := map[string]struct{}{}
for _, hostport := range hosts {
set[hostport] = struct{}{}
}
var keys []string
for key := range set {
keys = append(keys, key)
}
return &dnsSRVProvider{
UnresolvedHosts: keys,
Resolver: resolver,
Logger: logger,
}
}
func (provider *dnsSRVProvider) Hosts() ([]string, error) {
var results []string
resolvedHosts := map[string][]string{}
for _, host := range provider.UnresolvedHosts {
hostParts := strings.Split(host, ".")
if len(hostParts) <= 2 {
return nil, fmt.Errorf("could not seperate host from domain %q", host)
}
serviceName := hostParts[0]
domain := strings.Join(hostParts[1:], ".")
resolved, exists := resolvedHosts[serviceName]
if !exists {
_, srvs, err := provider.Resolver.LookupSRV(context.Background(), serviceName, "tcp", domain)
if err != nil {
return nil, fmt.Errorf("could not resolve host: %s.%s", serviceName, domain)
}
var targets []string
for _, s := range srvs {
addrs, err := provider.Resolver.LookupHost(context.Background(), s.Target)
if err != nil {
provider.Logger.Warn("could not resolve srv dns host", tag.Address(s.Target), tag.Error(err))
continue
}
for _, a := range addrs {
targets = append(targets, net.JoinHostPort(a, fmt.Sprintf("%d", s.Port)))
}
}
resolvedHosts[serviceName] = targets
resolved = targets
}
results = append(results, resolved...)
}
if len(results) == 0 {
return nil, errors.New("no hosts found, and bootstrap requires at least one")
}
return results, nil
}
func newDiscoveryProvider(
cfg *Config,
logger log.Logger,
) (discovery.DiscoverProvider, error) {
if cfg.DiscoveryProvider != nil {
// custom discovery provider takes first precedence
return cfg.DiscoveryProvider, nil
}
switch cfg.BootstrapMode {
case BootstrapModeHosts:
return statichosts.New(cfg.BootstrapHosts...), nil
case BootstrapModeFile:
return jsonfile.New(cfg.BootstrapFile), nil
case BootstrapModeDNS:
return newDNSProvider(cfg.BootstrapHosts, net.DefaultResolver, logger), nil
case BootstrapModeDNSSRV:
return newDNSSRVProvider(cfg.BootstrapHosts, net.DefaultResolver, logger), nil
}
return nil, fmt.Errorf("unknown bootstrap mode %q", cfg.BootstrapMode)
}