agent: use github.com/hashicorp/go-discover

Replace the provider specific node discovery code
with go-discover to support AWS, Azure and GCE.

Fixes #3282
This commit is contained in:
Frank Schroeder 2017-07-15 14:15:59 -07:00 committed by Frank Schröder
parent 5ddcdd41c2
commit 68e8f3d0f7
13 changed files with 253 additions and 519 deletions

View File

@ -10,6 +10,7 @@ import (
"net"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
@ -21,6 +22,7 @@ import (
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/consul/watch"
discover "github.com/hashicorp/go-discover"
"github.com/hashicorp/go-sockaddr/template"
"github.com/mitchellh/mapstructure"
)
@ -562,7 +564,7 @@ type Config struct {
StartJoinWan []string `mapstructure:"start_join_wan"`
// RetryJoin is a list of addresses to join with retry enabled.
RetryJoin []string `mapstructure:"retry_join"`
RetryJoin []string `mapstructure:"retry_join" json:"-"`
// RetryMaxAttempts specifies the maximum number of times to retry joining a
// host on startup. This is useful for cases where we know the node will be
@ -575,15 +577,6 @@ type Config struct {
RetryInterval time.Duration `mapstructure:"-" json:"-"`
RetryIntervalRaw string `mapstructure:"retry_interval"`
// RetryJoinEC2 specifies the configuration for auto-join on EC2.
RetryJoinEC2 RetryJoinEC2 `mapstructure:"retry_join_ec2"`
// RetryJoinGCE specifies the configuration for auto-join on GCE.
RetryJoinGCE RetryJoinGCE `mapstructure:"retry_join_gce"`
// RetryJoinAzure specifies the configuration for auto-join on Azure.
RetryJoinAzure RetryJoinAzure `mapstructure:"retry_join_azure"`
// RetryJoinWan is a list of addresses to join -wan with retry enabled.
RetryJoinWan []string `mapstructure:"retry_join_wan"`
@ -786,6 +779,9 @@ type Config struct {
DeprecatedAtlasJoin bool `mapstructure:"atlas_join" json:"-"`
DeprecatedAtlasEndpoint string `mapstructure:"atlas_endpoint" json:"-"`
DeprecatedHTTPAPIResponseHeaders map[string]string `mapstructure:"http_api_response_headers"`
DeprecatedRetryJoinEC2 RetryJoinEC2 `mapstructure:"retry_join_ec2"`
DeprecatedRetryJoinGCE RetryJoinGCE `mapstructure:"retry_join_gce"`
DeprecatedRetryJoinAzure RetryJoinAzure `mapstructure:"retry_join_azure"`
}
// IncomingHTTPSConfig returns the TLS configuration for HTTPS
@ -1185,6 +1181,65 @@ func DecodeConfig(r io.Reader) (*Config, error) {
"is no longer used. Please remove it from your configuration.")
}
if !reflect.DeepEqual(result.DeprecatedRetryJoinEC2, RetryJoinEC2{}) {
m := discover.Config{
"provider": "aws",
"region": result.DeprecatedRetryJoinEC2.Region,
"tag_key": result.DeprecatedRetryJoinEC2.TagKey,
"tag_value": result.DeprecatedRetryJoinEC2.TagValue,
"access_key_id": result.DeprecatedRetryJoinEC2.AccessKeyID,
"secret_access_key": result.DeprecatedRetryJoinEC2.SecretAccessKey,
}
result.RetryJoin = append(result.RetryJoin, m.String())
result.DeprecatedRetryJoinEC2 = RetryJoinEC2{}
// redact m before output
m["access_key_id"] = "<hidden>"
m["secret_access_key"] = "<hidden>"
fmt.Fprintf(os.Stderr, "==> DEPRECATION: retry_join_ec2 is deprecated."+
"Please add %q to retry_join\n", m)
}
if !reflect.DeepEqual(result.DeprecatedRetryJoinAzure, RetryJoinAzure{}) {
m := discover.Config{
"provider": "azure",
"tag_name": result.DeprecatedRetryJoinAzure.TagName,
"tag_value": result.DeprecatedRetryJoinAzure.TagValue,
"subscription_id": result.DeprecatedRetryJoinAzure.SubscriptionID,
"tenant_id": result.DeprecatedRetryJoinAzure.TenantID,
"client_id": result.DeprecatedRetryJoinAzure.ClientID,
"secret_access_key": result.DeprecatedRetryJoinAzure.SecretAccessKey,
}
result.RetryJoin = append(result.RetryJoin, m.String())
result.DeprecatedRetryJoinAzure = RetryJoinAzure{}
// redact m before output
m["subscription_id"] = "<hidden>"
m["tenant_id"] = "<hidden>"
m["client_id"] = "<hidden>"
m["secret_access_key"] = "<hidden>"
fmt.Fprintf(os.Stderr, "==> DEPRECATION: retry_join_azure is deprecated."+
"Please add %q to retry_join\n", m)
}
if !reflect.DeepEqual(result.DeprecatedRetryJoinGCE, RetryJoinGCE{}) {
m := discover.Config{
"provider": "gce",
"project_name": result.DeprecatedRetryJoinGCE.ProjectName,
"zone_pattern": result.DeprecatedRetryJoinGCE.ZonePattern,
"tag_value": result.DeprecatedRetryJoinGCE.TagValue,
"credentials_file": result.DeprecatedRetryJoinGCE.CredentialsFile,
}
result.RetryJoin = append(result.RetryJoin, m.String())
result.DeprecatedRetryJoinGCE = RetryJoinGCE{}
// redact m before output
m["credentials_file"] = "<hidden>"
fmt.Fprintf(os.Stderr, "==> DEPRECATION: retry_join_gce is deprecated."+
"Please add %q to retry_join\n", m)
}
// Check unused fields and verify that no bad configuration options were
// passed to Consul. There are a few additional fields which don't directly
// use mapstructure decoding, so we need to account for those as well. These
@ -1843,50 +1898,50 @@ func MergeConfig(a, b *Config) *Config {
if b.RetryInterval != 0 {
result.RetryInterval = b.RetryInterval
}
if b.RetryJoinEC2.AccessKeyID != "" {
result.RetryJoinEC2.AccessKeyID = b.RetryJoinEC2.AccessKeyID
if b.DeprecatedRetryJoinEC2.AccessKeyID != "" {
result.DeprecatedRetryJoinEC2.AccessKeyID = b.DeprecatedRetryJoinEC2.AccessKeyID
}
if b.RetryJoinEC2.SecretAccessKey != "" {
result.RetryJoinEC2.SecretAccessKey = b.RetryJoinEC2.SecretAccessKey
if b.DeprecatedRetryJoinEC2.SecretAccessKey != "" {
result.DeprecatedRetryJoinEC2.SecretAccessKey = b.DeprecatedRetryJoinEC2.SecretAccessKey
}
if b.RetryJoinEC2.Region != "" {
result.RetryJoinEC2.Region = b.RetryJoinEC2.Region
if b.DeprecatedRetryJoinEC2.Region != "" {
result.DeprecatedRetryJoinEC2.Region = b.DeprecatedRetryJoinEC2.Region
}
if b.RetryJoinEC2.TagKey != "" {
result.RetryJoinEC2.TagKey = b.RetryJoinEC2.TagKey
if b.DeprecatedRetryJoinEC2.TagKey != "" {
result.DeprecatedRetryJoinEC2.TagKey = b.DeprecatedRetryJoinEC2.TagKey
}
if b.RetryJoinEC2.TagValue != "" {
result.RetryJoinEC2.TagValue = b.RetryJoinEC2.TagValue
if b.DeprecatedRetryJoinEC2.TagValue != "" {
result.DeprecatedRetryJoinEC2.TagValue = b.DeprecatedRetryJoinEC2.TagValue
}
if b.RetryJoinGCE.ProjectName != "" {
result.RetryJoinGCE.ProjectName = b.RetryJoinGCE.ProjectName
if b.DeprecatedRetryJoinGCE.ProjectName != "" {
result.DeprecatedRetryJoinGCE.ProjectName = b.DeprecatedRetryJoinGCE.ProjectName
}
if b.RetryJoinGCE.ZonePattern != "" {
result.RetryJoinGCE.ZonePattern = b.RetryJoinGCE.ZonePattern
if b.DeprecatedRetryJoinGCE.ZonePattern != "" {
result.DeprecatedRetryJoinGCE.ZonePattern = b.DeprecatedRetryJoinGCE.ZonePattern
}
if b.RetryJoinGCE.TagValue != "" {
result.RetryJoinGCE.TagValue = b.RetryJoinGCE.TagValue
if b.DeprecatedRetryJoinGCE.TagValue != "" {
result.DeprecatedRetryJoinGCE.TagValue = b.DeprecatedRetryJoinGCE.TagValue
}
if b.RetryJoinGCE.CredentialsFile != "" {
result.RetryJoinGCE.CredentialsFile = b.RetryJoinGCE.CredentialsFile
if b.DeprecatedRetryJoinGCE.CredentialsFile != "" {
result.DeprecatedRetryJoinGCE.CredentialsFile = b.DeprecatedRetryJoinGCE.CredentialsFile
}
if b.RetryJoinAzure.TagName != "" {
result.RetryJoinAzure.TagName = b.RetryJoinAzure.TagName
if b.DeprecatedRetryJoinAzure.TagName != "" {
result.DeprecatedRetryJoinAzure.TagName = b.DeprecatedRetryJoinAzure.TagName
}
if b.RetryJoinAzure.TagValue != "" {
result.RetryJoinAzure.TagValue = b.RetryJoinAzure.TagValue
if b.DeprecatedRetryJoinAzure.TagValue != "" {
result.DeprecatedRetryJoinAzure.TagValue = b.DeprecatedRetryJoinAzure.TagValue
}
if b.RetryJoinAzure.SubscriptionID != "" {
result.RetryJoinAzure.SubscriptionID = b.RetryJoinAzure.SubscriptionID
if b.DeprecatedRetryJoinAzure.SubscriptionID != "" {
result.DeprecatedRetryJoinAzure.SubscriptionID = b.DeprecatedRetryJoinAzure.SubscriptionID
}
if b.RetryJoinAzure.TenantID != "" {
result.RetryJoinAzure.TenantID = b.RetryJoinAzure.TenantID
if b.DeprecatedRetryJoinAzure.TenantID != "" {
result.DeprecatedRetryJoinAzure.TenantID = b.DeprecatedRetryJoinAzure.TenantID
}
if b.RetryJoinAzure.ClientID != "" {
result.RetryJoinAzure.ClientID = b.RetryJoinAzure.ClientID
if b.DeprecatedRetryJoinAzure.ClientID != "" {
result.DeprecatedRetryJoinAzure.ClientID = b.DeprecatedRetryJoinAzure.ClientID
}
if b.RetryJoinAzure.SecretAccessKey != "" {
result.RetryJoinAzure.SecretAccessKey = b.RetryJoinAzure.SecretAccessKey
if b.DeprecatedRetryJoinAzure.SecretAccessKey != "" {
result.DeprecatedRetryJoinAzure.SecretAccessKey = b.DeprecatedRetryJoinAzure.SecretAccessKey
}
if b.RetryMaxAttemptsWan != 0 {
result.RetryMaxAttemptsWan = b.RetryMaxAttemptsWan

View File

@ -1,73 +0,0 @@
package agent
import (
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/defaults"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
)
// discoverEc2Hosts searches an AWS region, returning a list of instance ips
// where EC2TagKey = EC2TagValue
func (c *Config) discoverEc2Hosts(logger *log.Logger) ([]string, error) {
config := c.RetryJoinEC2
ec2meta := ec2metadata.New(session.New())
if config.Region == "" {
logger.Printf("[INFO] agent: No EC2 region provided, querying instance metadata endpoint...")
identity, err := ec2meta.GetInstanceIdentityDocument()
if err != nil {
return nil, err
}
config.Region = identity.Region
}
awsConfig := &aws.Config{
Region: &config.Region,
Credentials: credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.StaticProvider{
Value: credentials.Value{
AccessKeyID: config.AccessKeyID,
SecretAccessKey: config.SecretAccessKey,
},
},
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{},
defaults.RemoteCredProvider(*(defaults.Config()), defaults.Handlers()),
}),
}
svc := ec2.New(session.New(), awsConfig)
resp, err := svc.DescribeInstances(&ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("tag:" + config.TagKey),
Values: []*string{
aws.String(config.TagValue),
},
},
},
})
if err != nil {
return nil, err
}
var servers []string
for i := range resp.Reservations {
for _, instance := range resp.Reservations[i].Instances {
// Terminated instances don't have the PrivateIpAddress field
if instance.PrivateIpAddress != nil {
servers = append(servers, *instance.PrivateIpAddress)
}
}
}
return servers, nil
}

View File

@ -1,60 +0,0 @@
package agent
import (
"fmt"
"log"
"github.com/Azure/azure-sdk-for-go/arm/network"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
)
// discoverAzureHosts searches an Azure Subscription, returning a list of instance ips
// where AzureTag_Name = AzureTag_Value
func (c *Config) discoverAzureHosts(logger *log.Logger) ([]string, error) {
var servers []string
// Only works for the Azure PublicCLoud for now; no ability to test other Environment
oauthConfig, err := azure.PublicCloud.OAuthConfigForTenant(c.RetryJoinAzure.TenantID)
if err != nil {
return nil, err
}
// Get the ServicePrincipalToken for use searching the NetworkInterfaces
sbt, tokerr := azure.NewServicePrincipalToken(*oauthConfig,
c.RetryJoinAzure.ClientID,
c.RetryJoinAzure.SecretAccessKey,
azure.PublicCloud.ResourceManagerEndpoint,
)
if tokerr != nil {
return nil, tokerr
}
// Setup the client using autorest; followed the structure from Terraform
vmnet := network.NewInterfacesClient(c.RetryJoinAzure.SubscriptionID)
vmnet.Client.UserAgent = fmt.Sprint("Hashicorp-Consul")
vmnet.Authorizer = sbt
vmnet.Sender = autorest.CreateSender(autorest.WithLogging(logger))
// Get all Network interfaces across ResourceGroups unless there is a compelling reason to restrict
netres, neterr := vmnet.ListAll()
if neterr != nil {
return nil, neterr
}
// For now, ignore Primary interfaces, choose any PrivateIPAddress with the matching tags
for _, oneint := range *netres.Value {
// Make it a little more robust just in case there is actually no Tags
if oneint.Tags != nil {
tv := (*oneint.Tags)[c.RetryJoinAzure.TagName]
if tv != nil && *tv == c.RetryJoinAzure.TagValue {
// Make it a little more robust just in case IPConfigurations nil
if oneint.IPConfigurations != nil {
for _, onecfg := range *oneint.IPConfigurations {
// fmt.Println("Internal FQDN: ", *onecfg.Name, " IP: ", *onecfg.PrivateIPAddress)
// Only get the address if there is private IP address
if onecfg.PrivateIPAddress != nil {
servers = append(servers, *onecfg.PrivateIPAddress)
}
}
}
}
}
}
return servers, nil
}

View File

@ -1,43 +0,0 @@
package agent
import (
"log"
"os"
"testing"
)
func TestDiscoverAzureHosts(t *testing.T) {
subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID")
tenantID := os.Getenv("ARM_TENANT_ID")
clientID := os.Getenv("ARM_CLIENT_ID")
clientSecret := os.Getenv("ARM_CLIENT_SECRET")
environment := os.Getenv("ARM_ENVIRONMENT")
if subscriptionID == "" || clientID == "" || clientSecret == "" || tenantID == "" {
t.Skip("ARM_SUBSCRIPTION_ID, ARM_CLIENT_ID, ARM_CLIENT_SECRET and ARM_TENANT_ID " +
"must be set to test Discover Azure Hosts")
}
if environment == "" {
t.Log("Environments other than Public not supported at the moment")
}
c := &Config{
RetryJoinAzure: RetryJoinAzure{
SubscriptionID: subscriptionID,
ClientID: clientID,
SecretAccessKey: clientSecret,
TenantID: tenantID,
TagName: "type",
TagValue: "Foundation",
},
}
servers, err := c.discoverAzureHosts(log.New(os.Stderr, "", log.LstdFlags))
if err != nil {
t.Fatal(err)
}
if len(servers) != 3 {
t.Fatalf("bad: %v", servers)
}
}

View File

@ -1,38 +0,0 @@
package agent
import (
"log"
"os"
"testing"
)
func TestDiscoverEC2Hosts(t *testing.T) {
t.Parallel()
if os.Getenv("AWS_REGION") == "" {
t.Skip("AWS_REGION not set, skipping")
}
if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
t.Skip("AWS_ACCESS_KEY_ID not set, skipping")
}
if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
t.Skip("AWS_SECRET_ACCESS_KEY not set, skipping")
}
c := &Config{
RetryJoinEC2: RetryJoinEC2{
Region: os.Getenv("AWS_REGION"),
TagKey: "ConsulRole",
TagValue: "Server",
},
}
servers, err := c.discoverEc2Hosts(&log.Logger{})
if err != nil {
t.Fatal(err)
}
if len(servers) != 3 {
t.Fatalf("bad: %v", servers)
}
}

View File

@ -1,159 +0,0 @@
package agent
import (
"context"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
compute "google.golang.org/api/compute/v1"
)
// discoverGCEHosts searches a Google Compute Engine region, returning a list
// of instance ips that match the tags given in GCETags.
func (c *Config) discoverGCEHosts(logger *log.Logger) ([]string, error) {
config := c.RetryJoinGCE
ctx := oauth2.NoContext
var client *http.Client
var err error
logger.Printf("[INFO] agent: Initializing GCE client")
if config.CredentialsFile != "" {
logger.Printf("[INFO] agent: Loading credentials from %s", config.CredentialsFile)
key, err := ioutil.ReadFile(config.CredentialsFile)
if err != nil {
return nil, err
}
jwtConfig, err := google.JWTConfigFromJSON(key, compute.ComputeScope)
if err != nil {
return nil, err
}
client = jwtConfig.Client(ctx)
} else {
logger.Printf("[INFO] agent: Using default credential chain")
client, err = google.DefaultClient(ctx, compute.ComputeScope)
if err != nil {
return nil, err
}
}
computeService, err := compute.New(client)
if err != nil {
return nil, err
}
if config.ProjectName == "" {
logger.Printf("[INFO] agent: No GCE project provided, will discover from metadata.")
config.ProjectName, err = gceProjectIDFromMetadata(logger)
if err != nil {
return nil, err
}
} else {
logger.Printf("[INFO] agent: Using pre-defined GCE project name: %s", config.ProjectName)
}
zones, err := gceDiscoverZones(ctx, logger, computeService, config.ProjectName, config.ZonePattern)
if err != nil {
return nil, err
}
logger.Printf("[INFO] agent: Discovering GCE hosts with tag %s in zones: %s", config.TagValue, strings.Join(zones, ", "))
var servers []string
for _, zone := range zones {
addresses, err := gceInstancesAddressesForZone(ctx, logger, computeService, config.ProjectName, zone, config.TagValue)
if err != nil {
return nil, err
}
if len(addresses) > 0 {
logger.Printf("[INFO] agent: Discovered %d instances in %s/%s: %v", len(addresses), config.ProjectName, zone, addresses)
}
servers = append(servers, addresses...)
}
return servers, nil
}
// gceProjectIDFromMetadata queries the metadata service on GCE to get the
// project ID (name) of an instance.
func gceProjectIDFromMetadata(logger *log.Logger) (string, error) {
logger.Printf("[INFO] agent: Attempting to discover GCE project from metadata.")
client := &http.Client{}
req, err := http.NewRequest("GET", "http://metadata.google.internal/computeMetadata/v1/project/project-id", nil)
if err != nil {
return "", err
}
req.Header.Add("Metadata-Flavor", "Google")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
project, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
logger.Printf("[INFO] agent: GCE project discovered as: %s", project)
return string(project), nil
}
// gceDiscoverZones discovers a list of zones from a supplied zone pattern, or
// all of the zones available to a project.
func gceDiscoverZones(ctx context.Context, logger *log.Logger, computeService *compute.Service, project, pattern string) ([]string, error) {
var zones []string
if pattern != "" {
logger.Printf("[INFO] agent: Discovering zones for project %s matching pattern: %s", project, pattern)
} else {
logger.Printf("[INFO] agent: Discovering all zones available to project: %s", project)
}
call := computeService.Zones.List(project)
if pattern != "" {
call = call.Filter(fmt.Sprintf("name eq %s", pattern))
}
if err := call.Pages(ctx, func(page *compute.ZoneList) error {
for _, v := range page.Items {
zones = append(zones, v.Name)
}
return nil
}); err != nil {
return zones, err
}
logger.Printf("[INFO] agent: Discovered GCE zones: %s", strings.Join(zones, ", "))
return zones, nil
}
// gceInstancesAddressesForZone locates all instances within a specific project
// and zone, matching the supplied tag. Only the private IP addresses are
// returned, but ID is also logged.
func gceInstancesAddressesForZone(ctx context.Context, logger *log.Logger, computeService *compute.Service, project, zone, tag string) ([]string, error) {
var addresses []string
call := computeService.Instances.List(project, zone)
if err := call.Pages(ctx, func(page *compute.InstanceList) error {
for _, v := range page.Items {
for _, t := range v.Tags.Items {
if t == tag && len(v.NetworkInterfaces) > 0 && v.NetworkInterfaces[0].NetworkIP != "" {
addresses = append(addresses, v.NetworkInterfaces[0].NetworkIP)
}
}
}
return nil
}); err != nil {
return addresses, err
}
return addresses, nil
}

View File

@ -1,35 +0,0 @@
package agent
import (
"log"
"os"
"testing"
)
func TestDiscoverGCEHosts(t *testing.T) {
t.Parallel()
if os.Getenv("GCE_PROJECT") == "" {
t.Skip("GCE_PROJECT not set, skipping")
}
if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") == "" && os.Getenv("GCE_CONFIG_CREDENTIALS") == "" {
t.Skip("GOOGLE_APPLICATION_CREDENTIALS or GCE_CONFIG_CREDENTIALS not set, skipping")
}
c := &Config{
RetryJoinGCE: RetryJoinGCE{
ProjectName: os.Getenv("GCE_PROJECT"),
ZonePattern: os.Getenv("GCE_ZONE"),
TagValue: "consulrole-server",
CredentialsFile: os.Getenv("GCE_CONFIG_CREDENTIALS"),
},
}
servers, err := c.discoverGCEHosts(log.New(os.Stderr, "", log.LstdFlags))
if err != nil {
t.Fatal(err)
}
if len(servers) != 3 {
t.Fatalf("bad: %v", servers)
}
}

View File

@ -512,63 +512,63 @@ func TestDecodeConfig(t *testing.T) {
},
{
in: `{"retry_join_azure":{"client_id":"a"}}`,
c: &Config{RetryJoinAzure: RetryJoinAzure{ClientID: "a"}},
c: &Config{RetryJoin: []string{"provider=azure client_id=a"}},
},
{
in: `{"retry_join_azure":{"tag_name":"a"}}`,
c: &Config{RetryJoinAzure: RetryJoinAzure{TagName: "a"}},
c: &Config{RetryJoin: []string{"provider=azure tag_name=a"}},
},
{
in: `{"retry_join_azure":{"tag_value":"a"}}`,
c: &Config{RetryJoinAzure: RetryJoinAzure{TagValue: "a"}},
c: &Config{RetryJoin: []string{"provider=azure tag_value=a"}},
},
{
in: `{"retry_join_azure":{"secret_access_key":"a"}}`,
c: &Config{RetryJoinAzure: RetryJoinAzure{SecretAccessKey: "a"}},
c: &Config{RetryJoin: []string{"provider=azure secret_access_key=a"}},
},
{
in: `{"retry_join_azure":{"subscription_id":"a"}}`,
c: &Config{RetryJoinAzure: RetryJoinAzure{SubscriptionID: "a"}},
c: &Config{RetryJoin: []string{"provider=azure subscription_id=a"}},
},
{
in: `{"retry_join_azure":{"tenant_id":"a"}}`,
c: &Config{RetryJoinAzure: RetryJoinAzure{TenantID: "a"}},
c: &Config{RetryJoin: []string{"provider=azure tenant_id=a"}},
},
{
in: `{"retry_join_ec2":{"access_key_id":"a"}}`,
c: &Config{RetryJoinEC2: RetryJoinEC2{AccessKeyID: "a"}},
c: &Config{RetryJoin: []string{"provider=aws access_key_id=a"}},
},
{
in: `{"retry_join_ec2":{"region":"a"}}`,
c: &Config{RetryJoinEC2: RetryJoinEC2{Region: "a"}},
c: &Config{RetryJoin: []string{"provider=aws region=a"}},
},
{
in: `{"retry_join_ec2":{"tag_key":"a"}}`,
c: &Config{RetryJoinEC2: RetryJoinEC2{TagKey: "a"}},
c: &Config{RetryJoin: []string{"provider=aws tag_key=a"}},
},
{
in: `{"retry_join_ec2":{"tag_value":"a"}}`,
c: &Config{RetryJoinEC2: RetryJoinEC2{TagValue: "a"}},
c: &Config{RetryJoin: []string{"provider=aws tag_value=a"}},
},
{
in: `{"retry_join_ec2":{"secret_access_key":"a"}}`,
c: &Config{RetryJoinEC2: RetryJoinEC2{SecretAccessKey: "a"}},
c: &Config{RetryJoin: []string{"provider=aws secret_access_key=a"}},
},
{
in: `{"retry_join_gce":{"credentials_file":"a"}}`,
c: &Config{RetryJoinGCE: RetryJoinGCE{CredentialsFile: "a"}},
c: &Config{RetryJoin: []string{"provider=gce credentials_file=a"}},
},
{
in: `{"retry_join_gce":{"project_name":"a"}}`,
c: &Config{RetryJoinGCE: RetryJoinGCE{ProjectName: "a"}},
c: &Config{RetryJoin: []string{"provider=gce project_name=a"}},
},
{
in: `{"retry_join_gce":{"tag_value":"a"}}`,
c: &Config{RetryJoinGCE: RetryJoinGCE{TagValue: "a"}},
c: &Config{RetryJoin: []string{"provider=gce tag_value=a"}},
},
{
in: `{"retry_join_gce":{"zone_pattern":"a"}}`,
c: &Config{RetryJoinGCE: RetryJoinGCE{ZonePattern: "a"}},
c: &Config{RetryJoin: []string{"provider=gce zone_pattern=a"}},
},
{
in: `{"retry_join_wan":["a","b"]}`,
@ -1316,7 +1316,7 @@ func TestMergeConfig(t *testing.T) {
CheckUpdateIntervalRaw: "8m",
RetryIntervalRaw: "10s",
RetryIntervalWanRaw: "10s",
RetryJoinEC2: RetryJoinEC2{
DeprecatedRetryJoinEC2: RetryJoinEC2{
Region: "us-east-1",
TagKey: "Key1",
TagValue: "Value1",
@ -1465,7 +1465,7 @@ func TestMergeConfig(t *testing.T) {
Perms: "0700",
},
},
RetryJoinEC2: RetryJoinEC2{
DeprecatedRetryJoinEC2: RetryJoinEC2{
Region: "us-east-2",
TagKey: "Key2",
TagValue: "Value2",

View File

@ -2,59 +2,62 @@ package agent
import (
"fmt"
"strings"
"time"
discover "github.com/hashicorp/go-discover"
// support retry-join only for the following providers
// to add more providers import additional packages or 'all'
// to support all providers of go-discover
_ "github.com/hashicorp/go-discover/provider/aws"
_ "github.com/hashicorp/go-discover/provider/azure"
_ "github.com/hashicorp/go-discover/provider/gce"
)
// RetryJoin is used to handle retrying a join until it succeeds or all
// retries are exhausted.
func (a *Agent) retryJoin() {
cfg := a.config
ec2Enabled := cfg.RetryJoinEC2.TagKey != "" && cfg.RetryJoinEC2.TagValue != ""
gceEnabled := cfg.RetryJoinGCE.TagValue != ""
azureEnabled := cfg.RetryJoinAzure.TagName != "" && cfg.RetryJoinAzure.TagValue != ""
if len(cfg.RetryJoin) == 0 && !ec2Enabled && !gceEnabled && !azureEnabled {
if len(cfg.RetryJoin) == 0 {
return
}
a.logger.Printf("[INFO] agent: Supporting retry join for %v", discover.ProviderNames())
a.logger.Printf("[INFO] agent: Joining cluster...")
attempt := 0
for {
var servers []string
var addrs []string
var err error
switch {
case ec2Enabled:
servers, err = cfg.discoverEc2Hosts(a.logger)
if err != nil {
a.logger.Printf("[ERR] agent: Unable to query EC2 instances: %s", err)
for _, addr := range cfg.RetryJoin {
switch {
case strings.Contains(addr, "provider="):
servers, err := discover.Addrs(addr, a.logger)
if err != nil {
a.logger.Printf("[ERR] agent: %s", err)
} else {
addrs = append(addrs, servers...)
a.logger.Printf("[INFO] agent: Discovered servers: %s", strings.Join(servers, " "))
}
default:
addrs = append(addrs, addr)
}
a.logger.Printf("[INFO] agent: Discovered %d servers from EC2", len(servers))
case gceEnabled:
servers, err = cfg.discoverGCEHosts(a.logger)
if err != nil {
a.logger.Printf("[ERR] agent: Unable to query GCE instances: %s", err)
}
a.logger.Printf("[INFO] agent: Discovered %d servers from GCE", len(servers))
case azureEnabled:
servers, err = cfg.discoverAzureHosts(a.logger)
if err != nil {
a.logger.Printf("[ERR] agent: Unable to query Azure instances: %s", err)
}
a.logger.Printf("[INFO] agent: Discovered %d servers from Azure", len(servers))
}
servers = append(servers, cfg.RetryJoin...)
if len(servers) == 0 {
err = fmt.Errorf("No servers to join")
} else {
n, err := a.JoinLAN(servers)
if len(addrs) > 0 {
n, err := a.JoinLAN(addrs)
if err == nil {
a.logger.Printf("[INFO] agent: Join completed. Synced with %d initial agents", n)
return
}
}
if len(addrs) == 0 {
err = fmt.Errorf("No servers to join")
}
attempt++
if cfg.RetryMaxAttempts > 0 && attempt > cfg.RetryMaxAttempts {
a.retryJoinCh <- fmt.Errorf("agent: max join retry exhausted, exiting")

18
agent/retry_join_test.go Normal file
View File

@ -0,0 +1,18 @@
package agent
import (
"reflect"
"testing"
discover "github.com/hashicorp/go-discover"
)
// if this test fails check the _ imports of go-discover/provider/* packages
// in retry_join.go
func TestGoDiscoverRegistration(t *testing.T) {
got := discover.ProviderNames()
want := []string{"aws", "azure", "gce"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got go-discover providers %v want %v", got, want)
}
}

View File

@ -135,23 +135,23 @@ func (cmd *AgentCommand) readConfig() *agent.Config {
"Maximum number of join attempts. Defaults to 0, which will retry indefinitely.")
f.StringVar(&retryInterval, "retry-interval", "",
"Time to wait between join attempts.")
f.StringVar(&cmdCfg.RetryJoinEC2.Region, "retry-join-ec2-region", "",
f.StringVar(&cmdCfg.DeprecatedRetryJoinEC2.Region, "retry-join-ec2-region", "",
"EC2 Region to discover servers in.")
f.StringVar(&cmdCfg.RetryJoinEC2.TagKey, "retry-join-ec2-tag-key", "",
f.StringVar(&cmdCfg.DeprecatedRetryJoinEC2.TagKey, "retry-join-ec2-tag-key", "",
"EC2 tag key to filter on for server discovery.")
f.StringVar(&cmdCfg.RetryJoinEC2.TagValue, "retry-join-ec2-tag-value", "",
f.StringVar(&cmdCfg.DeprecatedRetryJoinEC2.TagValue, "retry-join-ec2-tag-value", "",
"EC2 tag value to filter on for server discovery.")
f.StringVar(&cmdCfg.RetryJoinGCE.ProjectName, "retry-join-gce-project-name", "",
f.StringVar(&cmdCfg.DeprecatedRetryJoinGCE.ProjectName, "retry-join-gce-project-name", "",
"Google Compute Engine project to discover servers in.")
f.StringVar(&cmdCfg.RetryJoinGCE.ZonePattern, "retry-join-gce-zone-pattern", "",
f.StringVar(&cmdCfg.DeprecatedRetryJoinGCE.ZonePattern, "retry-join-gce-zone-pattern", "",
"Google Compute Engine region or zone to discover servers in (regex pattern).")
f.StringVar(&cmdCfg.RetryJoinGCE.TagValue, "retry-join-gce-tag-value", "",
f.StringVar(&cmdCfg.DeprecatedRetryJoinGCE.TagValue, "retry-join-gce-tag-value", "",
"Google Compute Engine tag value to filter on for server discovery.")
f.StringVar(&cmdCfg.RetryJoinGCE.CredentialsFile, "retry-join-gce-credentials-file", "",
f.StringVar(&cmdCfg.DeprecatedRetryJoinGCE.CredentialsFile, "retry-join-gce-credentials-file", "",
"Path to credentials JSON file to use with Google Compute Engine.")
f.StringVar(&cmdCfg.RetryJoinAzure.TagName, "retry-join-azure-tag-name", "",
f.StringVar(&cmdCfg.DeprecatedRetryJoinAzure.TagName, "retry-join-azure-tag-name", "",
"Azure tag name to filter on for server discovery.")
f.StringVar(&cmdCfg.RetryJoinAzure.TagValue, "retry-join-azure-tag-value", "",
f.StringVar(&cmdCfg.DeprecatedRetryJoinAzure.TagValue, "retry-join-azure-tag-value", "",
"Azure tag value to filter on for server discovery.")
f.Var((*configutil.AppendSliceValue)(&cmdCfg.RetryJoinWan), "retry-join-wan",
"Address of an agent to join -wan at start time with retries enabled. "+
@ -430,20 +430,6 @@ func (cmd *AgentCommand) readConfig() *agent.Config {
cmd.UI.Error("WARNING: Bootstrap mode enabled! Do not enable unless necessary")
}
// Need both tag key and value for EC2 discovery
if cfg.RetryJoinEC2.TagKey != "" || cfg.RetryJoinEC2.TagValue != "" {
if cfg.RetryJoinEC2.TagKey == "" || cfg.RetryJoinEC2.TagValue == "" {
cmd.UI.Error("tag key and value are both required for EC2 retry-join")
return nil
}
}
// EC2 and GCE discovery are mutually exclusive
if cfg.RetryJoinEC2.TagKey != "" && cfg.RetryJoinEC2.TagValue != "" && cfg.RetryJoinGCE.TagValue != "" {
cmd.UI.Error("EC2 and GCE discovery are mutually exclusive. Please provide one or the other.")
return nil
}
// Verify the node metadata entries are valid
if err := structs.ValidateMetadata(cfg.Meta); err != nil {
cmd.UI.Error(fmt.Sprintf("Failed to parse node metadata: %v", err))

View File

@ -199,6 +199,68 @@ will exit with an error at startup.
port number — for example: `[::1]:8301`. This is useful for cases where we
know the address will become available eventually.
As of Consul 0.9.1 the cloud provider specific discovery of nodes has been
moved to the https://github.com/hashicorp/go-discover library which provides
a unified query interface for different providers. To use retry join for a
supported cloud provider provide a `-retry-join 'provider=xxx key=val key=val
...'` parameter with the provider specific values as described below. This
can be combined with static IP addresses and names or even multiple
`go-discover` configurations for different providers. This deprecates and
replaces the `-retry-join-ec2-*`, `-retry-join-azure-*` and
`-retry-join-gce-*` parameters and their usage will be translated to a
corresponding `go-discover` config string.
The supported providers for retry join at this point are Amazon EC2,
Microsoft Azure, Google Cloud and Softlayer.
* For Amazon EC2 use:
`provider=aws tag_key=xxx tag_value=xxx [region=xxx] [access_key_id=xxx] [secret_access_key=xxx]`
This returns the first private IP address of all servers in the given region
which have the given `tag_key` and `tag_value`. If the region is omitted it
will be discovered through the local instance's [EC2 metadata
endpoint](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html).
Authentication is handled in the following order:
- Static credentials `acesss_key_id=xxx secret_access_key=xxx`
- Environment variables (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`)
- Shared credentials file (`~/.aws/credentials` or the path specified by `AWS_SHARED_CREDENTIALS_FILE`)
- ECS task role metadata (container-specific).
- EC2 instance role metadata.
The only required IAM permission is `ec2:DescribeInstances`, and it is
recommended that you make a dedicated key used only for auto-joining.
* For Microsoft Azure use:
`provider=azure tag_name=xxx tag_value=xxx tenant_id=xxx client_id=xxx subscription_id=xxx secret_access_key=xxx`
This returns the first private IP address of all servers for the given
tenant/client/sucbscription with the given `tag_name` and `tag_value`.
* For Google Cloud (GCE) use:
`provider=gce project_name=xxx tag_value=xxx [zone_pattern=xxx] [credentials_file=xxx]`
This returns the first private IP address of all servers in the given project
which have the given `tag_value`. The list of zones can be restricted through
an RE2 compatible regular expression. If omitted, servers in all zones are
returned.
The discovery requires a
[GCE Service Account](https://cloud.google.com/compute/docs/access/service-accounts)
for which the credentials are searched in the following locations:
- Use credentials from `credentials_file`, if provided.
- Use JSON file from `GOOGLE_APPLICATION_CREDENTIALS` environment variable.
- Use JSON file in a location known to the gcloud command-line tool.
On Windows, this is `%APPDATA%/gcloud/application_default_credentials.json`.
On other systems, `$HOME/.config/gcloud/application_default_credentials.json`.
- On Google Compute Engine, use credentials from the metadata
server. In this final case any provided scopes are ignored.
* <a name="_retry_join_ec2_tag_key"></a><a href="#_retry_join_ec2_tag_key">`-retry-join-ec2-tag-key`
</a> - The Amazon EC2 instance tag key to filter on. When used with
[`-retry-join-ec2-tag-value`](#_retry_join_ec2_tag_value), Consul will attempt to join EC2
@ -213,32 +275,44 @@ will exit with an error at startup.
The only required IAM permission is `ec2:DescribeInstances`, and it is recommended you make a dedicated
key used only for auto-joining.
This parameter has been deprecated as of Consul 0.9.1. See [-retry-join](#_retry_join) for details.
* <a name="_retry_join_ec2_tag_value"></a><a href="#_retry_join_ec2_tag_value">`-retry-join-ec2-tag-value`
</a> - The Amazon EC2 instance tag value to filter on.
This parameter has been deprecated as of Consul 0.9.1. See [-retry-join](#_retry_join) for details.
* <a name="_retry_join_ec2_region"></a><a href="#_retry_join_ec2_region">`-retry-join-ec2-region`
</a> - (Optional) The Amazon EC2 region to use. If not specified, Consul
will use the local instance's [EC2 metadata endpoint](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html)
to discover the region.
This parameter has been deprecated as of Consul 0.9.1. See [-retry-join](#_retry_join) for details.
* <a name="_retry_join_gce_tag_value"></a><a href="#_retry_join_gce_tag_value">`-retry-join-gce-tag-value`
</a> - A Google Compute Engine instance tag to filter on. Much like the
`-retry-join-ec2-*` options, this gives Consul the option of doing server
discovery on [Google Compute Engine](https://cloud.google.com/compute/) by
searching the tags assigned to any particular instance.
This parameter has been deprecated as of Consul 0.9.1. See [-retry-join](#_retry_join) for details.
* <a name="_retry_join_gce_project_name"></a><a href="#_retry_join_gce_project_name">`-retry-join-gce-project-name`
</a> - The project to search in for the tag supplied by
[`-retry-join-gce-tag-value`](#_retry_join_gce_tag_value). If this is run
from within a GCE instance, the default is the project the instance is
located in.
This parameter has been deprecated as of Consul 0.9.1. See [-retry-join](#_retry_join) for details.
* <a name="_retry_join_gce_zone_pattern"></a><a href="#_retry_join_gce_zone_pattern">`-retry-join-gce-zone-pattern`
</a> - A regular expression that indicates the zones the tag should be
searched in. For example, while `us-west1-a` would only search in
`us-west1-a`, `us-west1-.*` would search in `us-west1-a` and `us-west1-b`.
The default is to search globally.
This parameter has been deprecated as of Consul 0.9.1. See [-retry-join](#_retry_join) for details.
* <a name="_retry_join_gce_credentials_file"></a><a href="#_retry_join_gce_credentials_file">`-retry-join-gce-credentials-file`
</a> - The path to the JSON credentials file of the [GCE Service
Account](https://cloud.google.com/compute/docs/access/service-accounts) that
@ -252,6 +326,8 @@ will exit with an error at startup.
- If none of these exist and discovery is being run from a GCE instance, the
instance's configured service account will be used.
This parameter has been deprecated as of Consul 0.9.1. See [-retry-join](#_retry_join) for details.
* <a name="_retry_join_azure_tag_name"></a><a href="#_retry_join_azure_tag_name">`-retry-join-azure-tag-name`
</a> - The Azure instance tag name to filter on. When used with
[`-retry-join-azure-tag-value`](#_retry_join_azure_tag_value), Consul will attempt to join Azure
@ -261,9 +337,13 @@ will exit with an error at startup.
The only permission needed is the ListAll method for NetworkInterfaces. It is recommended you make a dedicated key used only for auto-joining.
This parameter has been deprecated as of Consul 0.9.1. See [-retry-join](#_retry_join) for details.
* <a name="_retry_join_azure_tag_value"></a><a href="#_retry_join_azure_tag_value">`-retry-join-azure-tag-value`
</a> - The Azure instance tag value to filter on.
This parameter has been deprecated as of Consul 0.9.1. See [-retry-join](#_retry_join) for details.
* <a name="_retry_interval"></a><a href="#_retry_interval">`-retry-interval`</a> - Time
to wait between join attempts. Defaults to 30s.