Skip to content

Commit

Permalink
Add support for CloudID
Browse files Browse the repository at this point in the history
To facilitate connecting to the Elastic Service (https://elastic.co/cloud),
add a configuration option to take the CloudID (https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html),
which is parsed and transformed into a regular address.
  • Loading branch information
karmi committed Jul 13, 2019
1 parent 4739503 commit ddd1954
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 1 deletion.
2 changes: 2 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ To configure the client, pass a Config object to the NewClient function:
elasticsearch.NewClient(cfg)
When using the Elastic Service (https://elastic.co/cloud), you can use CloudID instead of Addresses.
See the elasticsearch_integration_test.go file and the _examples folder for more information.
Call the Elasticsearch APIs by invoking the corresponding methods on the client:
Expand Down
48 changes: 47 additions & 1 deletion elasticsearch.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package elasticsearch

import (
"encoding/base64"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -28,6 +29,7 @@ type Config struct {
Username string // Username for HTTP Basic Authentication.
Password string // Password for HTTP Basic Authentication.

CloudID string // Endpoint for the Elastic Service (https://elastic.co/cloud).
Transport http.RoundTripper // The HTTP transport object.
Logger estransport.Logger // The logger object.
}
Expand Down Expand Up @@ -61,13 +63,36 @@ func NewDefaultClient() (*Client, error) {
// environment variable.
//
func NewClient(cfg Config) (*Client, error) {
var addrs []string

envAddrs := addrsFromEnvironment()

if len(envAddrs) > 0 && len(cfg.Addresses) > 0 {
return nil, errors.New("cannot create client: both ELASTICSEARCH_URL and Addresses are set")
}

addrs := append(envAddrs, cfg.Addresses...)
if len(envAddrs) > 0 && cfg.CloudID != "" {
return nil, errors.New("cannot create client: both ELASTICSEARCH_URL and CloudID are set")
}

if len(cfg.Addresses) > 0 && cfg.CloudID != "" {
return nil, errors.New("cannot create client: both Adresses and CloudID are set")
}

if cfg.CloudID != "" {
cloudAddrs, err := addrFromCloudID(cfg.CloudID)
if err != nil {
return nil, fmt.Errorf("cannot create client: cannot parse CloudID: %s", err)
}
addrs = append(addrs, cloudAddrs)
} else {
if len(envAddrs) > 0 {
addrs = append(envAddrs, envAddrs...)
}
if len(cfg.Addresses) > 0 {
addrs = append(envAddrs, cfg.Addresses...)
}
}

urls, err := addrsToURLs(addrs)
if err != nil {
Expand Down Expand Up @@ -127,3 +152,24 @@ func addrsToURLs(addrs []string) ([]*url.URL, error) {
}
return urls, nil
}

// addrFromCloudID extracts the Elasticsearch URL from CloudID.
// See: https://www.elastic.co/guide/en/cloud/current/ec-cloud-id.html
//
func addrFromCloudID(input string) (string, error) {
var (
port = 9243
scheme = "https://"
)

values := strings.Split(input, ":")
if len(values) != 2 {
return "", fmt.Errorf("unexpected format: %q", input)
}
data, err := base64.StdEncoding.DecodeString(values[1])
if err != nil {
return "", err
}
parts := strings.Split(string(data), "$")
return fmt.Sprintf("%s%s.%s:%d", scheme, parts[1], parts[0], port), nil
}
78 changes: 78 additions & 0 deletions elasticsearch_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package elasticsearch

import (
"encoding/base64"
"errors"
"net/http"
"net/url"
Expand Down Expand Up @@ -67,6 +68,44 @@ func TestClientConfiguration(t *testing.T) {
}
})

t.Run("With URL from environment and cfg.CloudID", func(t *testing.T) {
os.Setenv("ELASTICSEARCH_URL", "http://example.com")
defer func() { os.Setenv("ELASTICSEARCH_URL", "") }()

_, err := NewClient(Config{CloudID: "foobar="})
if err == nil {
t.Fatalf("Expected error, got: %v", err)
}
match, _ := regexp.MatchString("both .* are set", err.Error())
if !match {
t.Errorf("Expected error when addresses from environment and configuration are used together, got: %v", err)
}
})

t.Run("With cfg.Addresses and cfg.CloudID", func(t *testing.T) {
_, err := NewClient(Config{Addresses: []string{"http://localhost:8080//"}, CloudID: "foobar="})
if err == nil {
t.Fatalf("Expected error, got: %v", err)
}
match, _ := regexp.MatchString("both .* are set", err.Error())
if !match {
t.Errorf("Expected error when addresses from environment and configuration are used together, got: %v", err)
}
})

t.Run("With CloudID", func(t *testing.T) {
c, err := NewClient(Config{CloudID: "foo:YmFyLmNsb3VkLmVzLmlvJGFiYzEyMyRkZWY0NTY="})
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}

u := c.Transport.(*estransport.Client).URLs()[0].String()

if u != "https://abc123.bar.cloud.es.io:9243" {
t.Errorf("Unexpected URL, want=https://abc123.bar.cloud.es.io:9243, got=%s", u)
}
})

t.Run("With invalid URL", func(t *testing.T) {
u := ":foo"
_, err := NewClient(Config{Addresses: []string{u}})
Expand Down Expand Up @@ -192,6 +231,45 @@ func TestAddrsToURLs(t *testing.T) {
}
}

func TestCloudID(t *testing.T) {
t.Run("Parse", func(t *testing.T) {
input := "name:" + base64.StdEncoding.EncodeToString([]byte("host$es$kibana"))
expected := "https://es.host:9243"

actual, err := addrFromCloudID(input)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if actual != expected {
t.Errorf("Unexpected output, want=%q, got=%q", expected, actual)
}
})

t.Run("Invalid format", func(t *testing.T) {
input := "foobar"
_, err := addrFromCloudID(input)
if err == nil {
t.Errorf("Expected error for input %q, got %v", input, err)
}
match, _ := regexp.MatchString("unexpected format", err.Error())
if !match {
t.Errorf("Unexpected error string: %s", err)
}
})

t.Run("Invalid base64 value", func(t *testing.T) {
input := "foobar:xxxxx"
_, err := addrFromCloudID(input)
if err == nil {
t.Errorf("Expected error for input %q, got %v", input, err)
}
match, _ := regexp.MatchString("illegal base64 data", err.Error())
if !match {
t.Errorf("Unexpected error string: %s", err)
}
})
}

func TestVersion(t *testing.T) {
if Version == "" {
t.Error("Version is empty")
Expand Down

0 comments on commit ddd1954

Please sign in to comment.