Skip to content

Commit

Permalink
Client auth (#20)
Browse files Browse the repository at this point in the history
* Add authentication and end-to-end test on client

* Bump version and add entry to changelog

* add toggle for indices and primaries

* add feature description and args

* Added status code and error checking to client
  • Loading branch information
Curt Brink authored and davidbrota committed Sep 14, 2018
1 parent d411169 commit eaef31d
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 18 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## 0.1.1 - 2018-09-13
### Added
- Implemented client authentication
- Implemented toggles for primaries and indices
- Added status code and error checking to client requests

## 0.1.0 - 2018-08-28
### Added
- Initial version: Includes Metrics and Inventory data
2 changes: 2 additions & 0 deletions elasticsearch-config.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ instances:
use_ssl <true or false to use SSL. If true Certificate bundle must be supplied>
ca_bundle_dir: <directory for certificate authority bundle, must be included if use_ssl is true>
ca_bundle_file: <file for certificate authority bundle, must be included if use_ssl is true>
collect_indices <true or false to collect indices metrics. If true collect indices, else do not>
collect_primaries <true or false to collect primaries metrics. If true collect primaries, else do not>
labels:
role: elasticsearch
57 changes: 53 additions & 4 deletions src/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"

Expand All @@ -19,15 +20,27 @@ const (

// HTTPClient represents a single connection to an Elasticsearch host
type HTTPClient struct {
baseURL string
client *http.Client
baseURL string
useAuth bool
username string
password string
client *http.Client
}

// Client interface that assists in mocking for tests
type Client interface {
Request(string, interface{}) error
}

type errorResponse struct {
Error *errorBody `json:"error"`
}

type errorBody struct {
Type *string `json:"type"`
Reason *string `json:"reason"`
}

// NewClient creates a new Elasticsearch http client.
// The hostnameOverride parameter specifies a hostname that the client should connect to.
// Passing in an empty string causes the client to use the hostname specified in the command-line args. (default behavior)
Expand All @@ -38,7 +51,10 @@ func NewClient(hostnameOverride string) (*HTTPClient, error) {
}

return &HTTPClient{
client: httpClient,
client: httpClient,
useAuth: args.Username != "" || args.Password != "",
username: args.Username,
password: args.Password,
baseURL: func() string {
protocol := "http"
if args.UseSSL {
Expand All @@ -58,16 +74,49 @@ func NewClient(hostnameOverride string) (*HTTPClient, error) {
// Request takes an endpoint, makes a GET request to that endpoint,
// and parses the response JSON into a map, which it returns.
func (c *HTTPClient) Request(endpoint string, v interface{}) error {
response, err := c.client.Get(c.baseURL + endpoint)
request, err := http.NewRequest("GET", c.baseURL+endpoint, nil)
if err != nil {
return err
}
if c.useAuth {
request.SetBasicAuth(c.username, c.password)
}

response, err := c.client.Do(request)
if err != nil {
return err
}
defer checkErr(response.Body.Close)

err = c.checkStatusCode(response)
if err != nil {
return err
}

err = json.NewDecoder(response.Body).Decode(v)
if err != nil {
return err
}

return nil
}

func (c *HTTPClient) checkStatusCode(response *http.Response) error {
if response.StatusCode == 200 {
return nil
}

// try parsing error in body, otherwise return generic error
responseBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("status code %v - could not parse body from response: %v", response.StatusCode, err)
}

var errResponse errorResponse
err = json.Unmarshal(responseBytes, &errResponse)
if err != nil {
return fmt.Errorf("status code %v - could not parse error information from response: %v", response.StatusCode, err)
}

return fmt.Errorf("status code %v - received error of type '%s' from Elasticsearch: %s", response.StatusCode, *errResponse.Error.Type, *errResponse.Error.Reason)
}
50 changes: 50 additions & 0 deletions src/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -66,3 +68,51 @@ func TestBadCertFile(t *testing.T) {
_, err := NewClient("")
assert.Error(t, err)
}

func TestAuthRequest(t *testing.T) {
// generate a test server so we can capture and inspect the request
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(200)
username, password, ok := req.BasicAuth()
assert.True(t, ok)
assert.Equal(t, username, "testUser")
assert.Equal(t, password, "testPass")
res.Write([]byte("{\"ok\":true}"))
}))
defer func() { testServer.Close() }()

client := &HTTPClient{
client: testServer.Client(),
useAuth: true,
username: "testUser",
password: "testPass",
baseURL: testServer.URL,
}

testResult := struct {
OK *bool `json:"ok"`
}{}

err := client.Request("/endpoint", &testResult)
assert.NoError(t, err)
assert.Equal(t, true, *testResult.OK)
}

func TestBadStatusCode(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(401)
res.Write([]byte("{\"error\":{\"type\":\"exception\",\"reason\":\"this is an error\"}}"))
}))
defer func() { testServer.Close() }()

client := &HTTPClient{
client: testServer.Client(),
useAuth: true,
username: "testUser",
password: "testPass",
baseURL: testServer.URL,
}

err := client.Request("/endpoint", nil)
assert.Error(t, err)
}
22 changes: 12 additions & 10 deletions src/elasticsearch.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@ import (

type argumentList struct {
sdkArgs.DefaultArgumentList
Hostname string `default:"localhost" help:"Hostname or IP where Elasticsearch Node is running."`
Port int `default:"9200" help:"Port on which Elasticsearch Node is listening."`
Username string `default:"" help:"Username for accessing Elasticsearch Node"`
Password string `default:"" help:"Password for the given user."`
UseSSL bool `default:"false" help:"Signals whether to use SSL or not. Certificate bundle must be supplied"`
CABundleFile string `default:"" help:"Alternative Certificate Authority bundle file"`
CABundleDir string `default:"" help:"Alternative Certificate Authority bundle directory"`
Timeout int `default:"30" help:"Timeout for an API call"`
ConfigPath string `default:"/etc/elasticsearch/elasticsearch.yml" help:"Path to the ElasticSearch configuration .yml file."`
Hostname string `default:"localhost" help:"Hostname or IP where Elasticsearch Node is running."`
Port int `default:"9200" help:"Port on which Elasticsearch Node is listening."`
Username string `default:"" help:"Username for accessing Elasticsearch Node"`
Password string `default:"" help:"Password for the given user."`
UseSSL bool `default:"false" help:"Signals whether to use SSL or not. Certificate bundle must be supplied"`
CABundleFile string `default:"" help:"Alternative Certificate Authority bundle file"`
CABundleDir string `default:"" help:"Alternative Certificate Authority bundle directory"`
Timeout int `default:"30" help:"Timeout for an API call"`
ConfigPath string `default:"/etc/elasticsearch/elasticsearch.yml" help:"Path to the ElasticSearch configuration .yml file."`
CollectIndices bool `default:"true" help:"Signals whether to collect indices metrics or not"`
CollectPrimaries bool `default:"true" help:"Signals whether to collect primaries metrics or not"`
}

const (
integrationName = "com.newrelic.elasticsearch"
integrationVersion = "0.1.0"
integrationVersion = "0.1.1"
)

var (
Expand Down
14 changes: 10 additions & 4 deletions src/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ func populateMetrics(i *integration.Integration, client Client) {
log.Error("There was an error populating metrics for common metrics: %v", err)
}

err = populateIndicesMetrics(i, client, commonResponse)
if err != nil {
log.Error("There was an error populating metrics for indices: %v", err)
if args.CollectIndices {
err = populateIndicesMetrics(i, client, commonResponse)
if err != nil {
log.Error("There was an error populating metrics for indices: %v", err)
}
}
}

Expand Down Expand Up @@ -77,7 +79,11 @@ func populateCommonMetrics(i *integration.Integration, client Client) (*CommonMe
return nil, err
}

return commonResponse, setMetricsResponse(i, commonResponse.All, "commonMetrics", "common")
if args.CollectPrimaries {
err = setMetricsResponse(i, commonResponse.All, "commonMetrics", "common")
}

return commonResponse, err
}

func populateIndicesMetrics(i *integration.Integration, client Client, commonStats *CommonMetrics) error {
Expand Down
2 changes: 2 additions & 0 deletions src/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ func TestPopulateClusterMetrics_Error(t *testing.T) {
func TestPopulateCommonMetrics(t *testing.T) {
i := getTestingIntegration(t)
client := createNewTestClient()
args.CollectIndices = true
args.CollectPrimaries = true
client.init("commonMetricsResult.json", commonStatsEndpoint, t)

populateCommonMetrics(i, client)
Expand Down

0 comments on commit eaef31d

Please sign in to comment.