Skip to content

Commit e57702f

Browse files
authored
Add product check for 8.x (#324)
* Client: Implement product check * Update compatibility matrix to reflect supported versions
1 parent 75ced8d commit e57702f

9 files changed

+218
-51
lines changed

.doc/installation.asciidoc

+4-30
Original file line numberDiff line numberDiff line change
@@ -53,34 +53,8 @@ go run main.go
5353
[discrete]
5454
=== {es} Version Compatibility
5555

56-
The client major versions correspond to the {es} major versions: to connect to
57-
{es} `7.x`, use a `7.x` version of the client, to connect to {es} `6.x`, use a
58-
`6.x` version of the client, and so on.
56+
Language clients are forward compatible; meaning that clients support communicating
57+
with greater minor versions of {es}.
5958

60-
[NOTE]
61-
--
62-
While the 5.x version of the client is available, it is no longer actively
63-
maintained, neither the corresponding {es} version. For more information, refer
64-
to https://www.elastic.co/support/eol[Elastic product end of life dates].
65-
--
66-
67-
[%header,cols=2*]
68-
|===
69-
|{es} Version
70-
|Client Version
71-
72-
|`master`
73-
|`master`
74-
75-
|`7.x`
76-
|`7.x`
77-
78-
|`6.x`
79-
|`6.x`
80-
81-
|`5.x`
82-
|`5.x`
83-
|===
84-
85-
The `master` branch of the client is compatible with the `master` branch of
86-
{es}.
59+
Elastic language clients are also backwards compatible with lesser supported
60+
minor {es} versions.

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ The official Go client for [Elasticsearch](https://www.elastic.co/products/elast
1212

1313
## Compatibility
1414

15-
The client major versions correspond to the compatible Elasticsearch major versions: to connect to Elasticsearch `7.x`, use a [`7.x`](https://github.com/elastic/go-elasticsearch/tree/7.x) version of the client, to connect to Elasticsearch `6.x`, use a [`6.x`](https://github.com/elastic/go-elasticsearch/tree/6.x) version of the client.
15+
Language clients are forward compatible; meaning that clients support communicating
16+
with greater minor versions of Elasticsearch.
17+
18+
Elastic language clients are also backwards compatible with lesser supported
19+
minor Elasticsearch versions.
1620

1721
When using Go modules, include the version in the import path, and specify either an explicit version or a branch:
1822

_examples/cloudfunction/function_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ type MockTransport struct{}
3535

3636
// RoundTrip returns a mock response.
3737
func (t *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
38-
return &http.Response{Body: ioutil.NopCloser(strings.NewReader(`{"status":"mocked"}`))}, nil
38+
return &http.Response{
39+
Body: ioutil.NopCloser(strings.NewReader(`{"status":"mocked"}`)),
40+
Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
41+
}, nil
3942
}
4043

4144
func TestHealth(t *testing.T) {

_examples/xkcdsearch/store_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func TestStore(t *testing.T) {
8181
Response: &http.Response{
8282
StatusCode: http.StatusOK,
8383
Body: ioutil.NopCloser(strings.NewReader(`{}`)),
84+
Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
8485
},
8586
}
8687
mocktrans.RoundTripFn = func(req *http.Request) (*http.Response, error) { return mocktrans.Response, nil }

elasticsearch.go

+58-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"net/url"
2626
"os"
2727
"strings"
28+
"sync"
2829
"time"
2930

3031
"github.com/elastic/go-elasticsearch/v8/esapi"
@@ -38,7 +39,10 @@ const (
3839

3940
// Version returns the package version as a string.
4041
//
41-
const Version = version.Client
42+
const (
43+
Version = version.Client
44+
unknownProduct = "the client noticed that the server is not Elasticsearch and we do not support this unknown product"
45+
)
4246

4347
// Config represents the client configuration.
4448
//
@@ -88,6 +92,9 @@ type Config struct {
8892
type Client struct {
8993
*esapi.API // Embeds the API methods
9094
Transport estransport.Interface
95+
96+
productCheckMu sync.RWMutex
97+
productCheckSuccess bool
9198
}
9299

93100
// NewDefaultClient creates a new client with default options.
@@ -187,7 +194,8 @@ func NewClient(cfg Config) (*Client, error) {
187194
return nil, fmt.Errorf("error creating transport: %s", err)
188195
}
189196

190-
client := &Client{Transport: tp, API: esapi.New(tp)}
197+
client := &Client{Transport: tp}
198+
client.API = esapi.New(client)
191199

192200
if cfg.DiscoverNodesOnStart {
193201
go client.DiscoverNodes()
@@ -199,7 +207,54 @@ func NewClient(cfg Config) (*Client, error) {
199207
// Perform delegates to Transport to execute a request and return a response.
200208
//
201209
func (c *Client) Perform(req *http.Request) (*http.Response, error) {
202-
return c.Transport.Perform(req)
210+
// Retrieve the original request.
211+
res, err := c.Transport.Perform(req)
212+
213+
// ResponseCheck path continues, we run the header check on the first answer from ES.
214+
if err == nil {
215+
checkHeader := func() error { return genuineCheckHeader(res.Header) }
216+
if err := c.doProductCheck(checkHeader); err != nil {
217+
res.Body.Close()
218+
return nil, err
219+
}
220+
}
221+
return res, err
222+
}
223+
224+
// doProductCheck calls f if there as not been a prior successful call to doProductCheck,
225+
// returning nil otherwise.
226+
func (c *Client) doProductCheck(f func() error) error {
227+
c.productCheckMu.RLock()
228+
productCheckSuccess := c.productCheckSuccess
229+
c.productCheckMu.RUnlock()
230+
231+
if productCheckSuccess {
232+
return nil
233+
}
234+
235+
c.productCheckMu.Lock()
236+
defer c.productCheckMu.Unlock()
237+
238+
if c.productCheckSuccess {
239+
return nil
240+
}
241+
242+
if err := f(); err != nil {
243+
return err
244+
}
245+
246+
c.productCheckSuccess = true
247+
248+
return nil
249+
}
250+
251+
// genuineCheckHeader validates the presence of the X-Elastic-Product header
252+
//
253+
func genuineCheckHeader(header http.Header) error {
254+
if header.Get("X-Elastic-Product") != "Elasticsearch" {
255+
return errors.New(unknownProduct)
256+
}
257+
return nil
203258
}
204259

205260
// Metrics returns the client metrics.

elasticsearch_benchmark_test.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ var defaultResponse = http.Response{
3535
Status: "200 OK",
3636
StatusCode: 200,
3737
ContentLength: 2,
38-
Header: http.Header(map[string][]string{"Content-Type": {"application/json"}}),
39-
Body: ioutil.NopCloser(strings.NewReader(`{}`)),
38+
Header: http.Header(map[string][]string{
39+
"Content-Type": {"application/json"},
40+
"X-Elastic-Product": {"Elasticsearch"},
41+
}),
42+
Body: ioutil.NopCloser(strings.NewReader(`{}`)),
4043
}
4144

4245
type FakeTransport struct {

elasticsearch_internal_test.go

+121-9
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,51 @@ package elasticsearch
2222
import (
2323
"encoding/base64"
2424
"errors"
25+
"io/ioutil"
2526
"net/http"
27+
"net/http/httptest"
2628
"net/url"
2729
"os"
30+
"reflect"
2831
"regexp"
32+
"strings"
2933
"testing"
3034

3135
"github.com/elastic/go-elasticsearch/v8/estransport"
3236
)
3337

38+
var called bool
39+
40+
type mockTransp struct {
41+
RoundTripFunc func(*http.Request) (*http.Response, error)
42+
}
43+
44+
var defaultRoundTripFunc = func(req *http.Request) (*http.Response, error) {
45+
response := &http.Response{Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}}}
46+
47+
if req.URL.Path == "/" {
48+
response.Body = ioutil.NopCloser(strings.NewReader(`{
49+
"version" : {
50+
"number" : "8.0.0-SNAPSHOT",
51+
"build_flavor" : "default"
52+
},
53+
"tagline" : "You Know, for Search"
54+
}`))
55+
response.Header.Add("Content-Type", "application/json")
56+
} else {
57+
called = true
58+
}
59+
60+
return response, nil
61+
}
62+
63+
func (t *mockTransp) RoundTrip(req *http.Request) (*http.Response, error) {
64+
if t.RoundTripFunc == nil {
65+
return defaultRoundTripFunc(req)
66+
}
67+
return t.RoundTripFunc(req)
68+
}
69+
3470
func TestClientConfiguration(t *testing.T) {
3571
t.Parallel()
3672

@@ -173,15 +209,6 @@ func TestClientConfiguration(t *testing.T) {
173209
})
174210
}
175211

176-
var called bool
177-
178-
type mockTransp struct{}
179-
180-
func (t *mockTransp) RoundTrip(req *http.Request) (*http.Response, error) {
181-
called = true
182-
return &http.Response{}, nil
183-
}
184-
185212
func TestClientInterface(t *testing.T) {
186213
t.Run("Transport", func(t *testing.T) {
187214
c, err := NewClient(Config{Transport: &mockTransp{}})
@@ -357,3 +384,88 @@ func TestClientMetrics(t *testing.T) {
357384
t.Errorf("Unexpected output: %s", m)
358385
}
359386
}
387+
388+
func TestResponseCheckOnly(t *testing.T) {
389+
tests := []struct {
390+
name string
391+
response *http.Response
392+
requestErr error
393+
wantErr bool
394+
}{
395+
{
396+
name: "Valid answer with header",
397+
response: &http.Response{
398+
Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
399+
Body: ioutil.NopCloser(strings.NewReader("{}")),
400+
},
401+
wantErr: false,
402+
},
403+
{
404+
name: "Valid answer without header",
405+
response: &http.Response{
406+
Body: ioutil.NopCloser(strings.NewReader("{}")),
407+
},
408+
wantErr: true,
409+
},
410+
{
411+
name: "Valid answer with http error code",
412+
response: &http.Response{
413+
StatusCode: http.StatusUnauthorized,
414+
Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
415+
Body: ioutil.NopCloser(strings.NewReader("{}")),
416+
},
417+
wantErr: false,
418+
},
419+
}
420+
421+
for _, tt := range tests {
422+
t.Run(tt.name, func(t *testing.T) {
423+
c, _ := NewClient(Config{
424+
Transport: &mockTransp{RoundTripFunc: func(request *http.Request) (*http.Response, error) {
425+
return tt.response, tt.requestErr
426+
}},
427+
})
428+
_, err := c.Cat.Indices()
429+
if (err != nil) != tt.wantErr {
430+
t.Errorf("Unexpected error, got %v, wantErr %v", err, tt.wantErr)
431+
}
432+
})
433+
}
434+
}
435+
436+
437+
func TestProductCheckError(t *testing.T) {
438+
var requestPaths []string
439+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
440+
requestPaths = append(requestPaths, r.URL.Path)
441+
if len(requestPaths) == 1 {
442+
// Simulate transient error from a proxy on the first request.
443+
// This must not be cached by the client.
444+
w.WriteHeader(http.StatusBadGateway)
445+
return
446+
}
447+
w.Header().Set("X-Elastic-Product", "Elasticsearch")
448+
w.Write([]byte("{}"))
449+
}))
450+
defer server.Close()
451+
452+
c, _ := NewClient(Config{Addresses: []string{server.URL}, DisableRetry: true})
453+
if _, err := c.Cat.Indices(); err == nil {
454+
t.Fatal("expected error")
455+
}
456+
if c.productCheckSuccess {
457+
t.Fatalf("product check should be invalid, got %v", c.productCheckSuccess)
458+
}
459+
if _, err := c.Cat.Indices(); err != nil {
460+
t.Fatalf("unexpected error: %s", err)
461+
}
462+
if n := len(requestPaths); n != 2 {
463+
t.Fatalf("expected 2 requests, got %d", n)
464+
}
465+
if !reflect.DeepEqual(requestPaths, []string{"/_cat/indices", "/_cat/indices"}) {
466+
t.Fatalf("unexpected request paths: %s", requestPaths)
467+
}
468+
if !c.productCheckSuccess {
469+
t.Fatalf("product check should be valid, got : %v", c.productCheckSuccess)
470+
}
471+
}

esapi/esapi_benchmark_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ import (
3232
// TODO(karmi): Refactor into a shared mock/testing package
3333

3434
var (
35-
defaultResponse = &http.Response{StatusCode: 200, Body: ioutil.NopCloser(strings.NewReader("MOCK"))}
35+
defaultResponse = &http.Response{
36+
StatusCode: 200,
37+
Body: ioutil.NopCloser(strings.NewReader("MOCK")),
38+
Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
39+
}
3640
defaultRoundTripFn = func(*http.Request) (*http.Response, error) { return defaultResponse, nil }
3741
errorRoundTripFn = func(*http.Request) (*http.Response, error) {
3842
return &http.Response{
@@ -54,6 +58,7 @@ var (
5458
},
5559
"status" : 400
5660
}`)),
61+
Header: http.Header{"X-Elastic-Product": []string{"Elasticsearch"}},
5762
}, nil
5863
}
5964
)

0 commit comments

Comments
 (0)