Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client: Implement 8.x response header check behaviour #291

Merged
merged 1 commit into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 39 additions & 21 deletions elasticsearch.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import (
)

var (
reVersion *regexp.Regexp
reVersion *regexp.Regexp
)

func init() {
Expand All @@ -47,9 +47,9 @@ func init() {
}

const (
defaultURL = "http://localhost:9200"
tagline = "You Know, for Search"
unknownProduct = "the client noticed that the server is not Elasticsearch and we do not support this unknown product"
defaultURL = "http://localhost:9200"
tagline = "You Know, for Search"
unknownProduct = "the client noticed that the server is not Elasticsearch and we do not support this unknown product"
)

// Version returns the package version as a string.
Expand Down Expand Up @@ -85,7 +85,8 @@ type Config struct {
EnableMetrics bool // Enable the metrics collection.
EnableDebugLogger bool // Enable the debug logging.

DisableMetaHeader bool // Disable the additional "X-Elastic-Client-Meta" HTTP header.
DisableMetaHeader bool // Disable the additional "X-Elastic-Client-Meta" HTTP header.
UseResponseCheckOnly bool

RetryBackoff func(attempt int) time.Duration // Optional backoff duration. Default: nil.

Expand All @@ -100,21 +101,23 @@ type Config struct {
// Client represents the Elasticsearch client.
//
type Client struct {
*esapi.API// Embeds the API methods
Transport estransport.Interface
*esapi.API // Embeds the API methods
Transport estransport.Interface

once sync.Once
productCheckError error
productCheckOnce sync.Once
responseCheckOnce sync.Once
productCheckError error
useResponseCheckOnly bool
}

type esVersion struct {
Number string `json:"number"`
BuildFlavor string `json:"build_flavor"`
Number string `json:"number"`
BuildFlavor string `json:"build_flavor"`
}

type info struct {
Version esVersion `json:"version"`
Tagline string `json:"tagline"`
Version esVersion `json:"version"`
Tagline string `json:"tagline"`
}

// NewDefaultClient creates a new client with default options.
Expand Down Expand Up @@ -212,7 +215,7 @@ func NewClient(cfg Config) (*Client, error) {
return nil, fmt.Errorf("error creating transport: %s", err)
}

client := &Client{Transport: tp}
client := &Client{Transport: tp, useResponseCheckOnly: cfg.UseResponseCheckOnly}
client.API = esapi.New(client)

if cfg.DiscoverNodesOnStart {
Expand Down Expand Up @@ -277,23 +280,38 @@ func ParseElasticsearchVersion(version string) (int64, int64, int64, error) {
// Perform delegates to Transport to execute a request and return a response.
//
func (c *Client) Perform(req *http.Request) (*http.Response, error) {
c.once.Do(func() {
err := c.productCheck()
if err != nil {
c.productCheckError = err
// ProductCheck validation
c.productCheckOnce.Do(func() {
// We skip this validation of we only want the header validation.
// ResponseCheck path continues after original request.
if c.useResponseCheckOnly {
return
}

// Launch product check for 7.x, request info, check header then payload.
c.productCheckError = c.productCheck()
return
})

// Retrieve the original request.
res, err := c.Transport.Perform(req)

c.responseCheckOnce.Do(func() {
// ResponseCheck path continues, we run the header check on the first answer from ES.
if c.useResponseCheckOnly {
c.productCheckError = genuineCheckHeader(res.Header)
}
})

if c.productCheckError != nil {
return nil, c.productCheckError
}

return c.Transport.Perform(req)
return res, err
}

// productCheck runs an esapi.Info query to retrieve informations of the current cluster
// decodes the response and decides if the cluster is a genuine Elasticsearch product.
func (c *Client) productCheck() (error) {
func (c *Client) productCheck() error {
var info info

req := esapi.InfoRequest{}
Expand Down
61 changes: 60 additions & 1 deletion elasticsearch_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,4 +568,63 @@ func TestGenuineCheckHeader(t *testing.T) {
}
})
}
}
}

func TestResponseCheckOnly(t *testing.T) {
tests := []struct {
name string
useResponseCheckOnly bool
response *http.Response
wantErr bool
} {
{
name: "Valid answer with header",
useResponseCheckOnly: false,
response: &http.Response{
Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
Body: ioutil.NopCloser(strings.NewReader("{}")),
},
wantErr: false,
},
{
name: "Valid answer without header",
useResponseCheckOnly: false,
response: &http.Response{
Body: ioutil.NopCloser(strings.NewReader("{}")),
},
wantErr: true,
},
{
name: "Valid answer with header and response check",
useResponseCheckOnly: true,
response: &http.Response{
Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
Body: ioutil.NopCloser(strings.NewReader("{}")),
},
wantErr: false,
},
{
name: "Valid answer withouth header and response check",
useResponseCheckOnly: true,
response: &http.Response{
Body: ioutil.NopCloser(strings.NewReader("{}")),
},
wantErr: true,
},

}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, _ := NewClient(Config{
Transport: &mockTransp{RoundTripFunc: func(request *http.Request) (*http.Response, error) {
return tt.response, nil
}},
UseResponseCheckOnly: tt.useResponseCheckOnly,
})
_, err := c.Cat.Indices()
if (err != nil) != tt.wantErr {
t.Errorf("Unexpected error, got %v, wantErr %v", err, tt.wantErr)
}
})
}
}