From 68e8f3d0f764db13dedef4e1aea54966114b900e Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Sat, 15 Jul 2017 14:15:59 -0700 Subject: [PATCH] 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 --- agent/config.go | 135 ++++++++++++------ agent/config_aws.go | 73 ---------- agent/config_azure.go | 60 -------- agent/config_azure_test.go | 43 ------ agent/config_ec2_test.go | 38 ------ agent/config_gce.go | 159 ---------------------- agent/config_gce_test.go | 35 ----- agent/config_test.go | 34 ++--- agent/retry_join.go | 63 +++++---- agent/retry_join_test.go | 18 +++ command/agent.go | 32 ++--- vendor/vendor.json | 2 +- website/source/docs/agent/options.html.md | 80 +++++++++++ 13 files changed, 253 insertions(+), 519 deletions(-) delete mode 100644 agent/config_aws.go delete mode 100644 agent/config_azure.go delete mode 100644 agent/config_azure_test.go delete mode 100644 agent/config_ec2_test.go delete mode 100644 agent/config_gce.go delete mode 100644 agent/config_gce_test.go create mode 100644 agent/retry_join_test.go diff --git a/agent/config.go b/agent/config.go index c6679064e..9662647e8 100644 --- a/agent/config.go +++ b/agent/config.go @@ -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"] = "" + m["secret_access_key"] = "" + + 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"] = "" + m["tenant_id"] = "" + m["client_id"] = "" + m["secret_access_key"] = "" + + 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"] = "" + + 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 diff --git a/agent/config_aws.go b/agent/config_aws.go deleted file mode 100644 index 4386fd5df..000000000 --- a/agent/config_aws.go +++ /dev/null @@ -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 -} diff --git a/agent/config_azure.go b/agent/config_azure.go deleted file mode 100644 index 1528b894e..000000000 --- a/agent/config_azure.go +++ /dev/null @@ -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 -} diff --git a/agent/config_azure_test.go b/agent/config_azure_test.go deleted file mode 100644 index f45170334..000000000 --- a/agent/config_azure_test.go +++ /dev/null @@ -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) - } -} diff --git a/agent/config_ec2_test.go b/agent/config_ec2_test.go deleted file mode 100644 index 2a529b752..000000000 --- a/agent/config_ec2_test.go +++ /dev/null @@ -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) - } -} diff --git a/agent/config_gce.go b/agent/config_gce.go deleted file mode 100644 index 16622048d..000000000 --- a/agent/config_gce.go +++ /dev/null @@ -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 -} diff --git a/agent/config_gce_test.go b/agent/config_gce_test.go deleted file mode 100644 index aeae75f24..000000000 --- a/agent/config_gce_test.go +++ /dev/null @@ -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) - } -} diff --git a/agent/config_test.go b/agent/config_test.go index 101f87b8f..c7a8f23f5 100644 --- a/agent/config_test.go +++ b/agent/config_test.go @@ -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", diff --git a/agent/retry_join.go b/agent/retry_join.go index a2bf36be5..dcf6c2dbc 100644 --- a/agent/retry_join.go +++ b/agent/retry_join.go @@ -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") diff --git a/agent/retry_join_test.go b/agent/retry_join_test.go new file mode 100644 index 000000000..703277e9c --- /dev/null +++ b/agent/retry_join_test.go @@ -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) + } +} diff --git a/command/agent.go b/command/agent.go index bea22a647..66c5ef308 100644 --- a/command/agent.go +++ b/command/agent.go @@ -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)) diff --git a/vendor/vendor.json b/vendor/vendor.json index 756948bf5..fd00f9428 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -85,4 +85,4 @@ {"checksumSHA1":"vlicYp+fe4ECQ+5QqpAk36VRA3s=","path":"golang.org/x/sys/unix","revision":"cd2c276457edda6df7fb04895d3fd6a6add42926","revisionTime":"2017-07-17T10:05:24Z"} ], "rootPath": "github.com/hashicorp/consul" -} +} \ No newline at end of file diff --git a/website/source/docs/agent/options.html.md b/website/source/docs/agent/options.html.md index 5c461ccba..6e4e9300a 100644 --- a/website/source/docs/agent/options.html.md +++ b/website/source/docs/agent/options.html.md @@ -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. + * `-retry-join-ec2-tag-key` - 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. + * `-retry-join-ec2-tag-value` - 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. + * `-retry-join-ec2-region` - (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. + * `-retry-join-gce-tag-value` - 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. + * `-retry-join-gce-project-name` - 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. + * `-retry-join-gce-zone-pattern` - 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. + * `-retry-join-gce-credentials-file` - 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. + * `-retry-join-azure-tag-name` - 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. + * `-retry-join-azure-tag-value` - 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. + * `-retry-interval` - Time to wait between join attempts. Defaults to 30s.