Skip to content

Commit 48f74c9

Browse files
jsorianomrodm
andauthored
Extend client APIs for support of existing stacks in more cases (#2338)
Move some methods from the serverless provider to the clients, so they can be more easily reused, and add some additional methods that can be useful to extend the support of existing stacks. Co-authored-by: Mario Rodriguez Molins <marrodmo@gmail.com>
1 parent 6463b0f commit 48f74c9

19 files changed

+928
-161
lines changed

internal/elasticsearch/client.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,37 @@ func (client *Client) CheckHealth(ctx context.Context) error {
189189
return nil
190190
}
191191

192+
type Info struct {
193+
Name string `json:"name"`
194+
ClusterName string `json:"cluster_name"`
195+
ClusterUUID string `json:"cluster_uuid"`
196+
Version struct {
197+
Number string `json:"number"`
198+
BuildFlavor string `json:"build_flavor"`
199+
} `json:"version`
200+
}
201+
202+
// Info gets cluster information and metadata.
203+
func (client *Client) Info(ctx context.Context) (*Info, error) {
204+
resp, err := client.Client.Info(client.Client.Info.WithContext(ctx))
205+
if err != nil {
206+
return nil, fmt.Errorf("error getting cluster info: %w", err)
207+
}
208+
defer resp.Body.Close()
209+
210+
if resp.StatusCode != http.StatusOK {
211+
return nil, fmt.Errorf("failed to get cluster info: %s", resp.String())
212+
}
213+
214+
var info Info
215+
err = json.NewDecoder(resp.Body).Decode(&info)
216+
if err != nil {
217+
return nil, fmt.Errorf("error decoding cluster info: %w", err)
218+
}
219+
220+
return &info, nil
221+
}
222+
192223
// IsFailureStoreAvailable checks if the failure store is available.
193224
func (client *Client) IsFailureStoreAvailable(ctx context.Context) (bool, error) {
194225
// FIXME: Using the low-level transport till the API SDK supports the failure store.

internal/elasticsearch/client_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ func TestClusterHealth(t *testing.T) {
9393
}
9494
}
9595

96+
func TestClusterInfo(t *testing.T) {
97+
client := test.NewClient(t, "./testdata/elasticsearch-9-info")
98+
info, err := client.Info(context.Background())
99+
require.NoError(t, err)
100+
assert.Equal(t, "9.0.0-SNAPSHOT", info.Version.Number)
101+
}
102+
96103
func writeCACertFile(t *testing.T, cert *x509.Certificate) string {
97104
var d bytes.Buffer
98105
err := pem.Encode(&d, &pem.Block{
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
---
2+
version: 2
3+
interactions:
4+
- id: 0
5+
request:
6+
proto: HTTP/1.1
7+
proto_major: 1
8+
proto_minor: 1
9+
content_length: 0
10+
transfer_encoding: []
11+
trailer: {}
12+
host: ""
13+
remote_addr: ""
14+
request_uri: ""
15+
body: ""
16+
form: {}
17+
headers:
18+
Authorization:
19+
- Basic ZWxhc3RpYzpjaGFuZ2VtZQ==
20+
User-Agent:
21+
- go-elasticsearch/7.17.10 (linux amd64; Go 1.23.4)
22+
X-Elastic-Client-Meta:
23+
- es=7.17.10,go=1.23.4,t=7.17.10,hc=1.23.4
24+
url: https://127.0.0.1:9200/
25+
method: GET
26+
response:
27+
proto: HTTP/1.1
28+
proto_major: 1
29+
proto_minor: 1
30+
transfer_encoding: []
31+
trailer: {}
32+
content_length: 547
33+
uncompressed: false
34+
body: |
35+
{
36+
"name" : "da1c4d2b1379",
37+
"cluster_name" : "elasticsearch",
38+
"cluster_uuid" : "KPcvXrW1Rnega2GT0sn3mg",
39+
"version" : {
40+
"number" : "9.0.0-SNAPSHOT",
41+
"build_flavor" : "default",
42+
"build_type" : "docker",
43+
"build_hash" : "873dc52360a9265824a70b3113c3dd350ff9249a",
44+
"build_date" : "2025-01-13T13:20:37.908330789Z",
45+
"build_snapshot" : true,
46+
"lucene_version" : "10.0.0",
47+
"minimum_wire_compatibility_version" : "8.18.0",
48+
"minimum_index_compatibility_version" : "8.0.0"
49+
},
50+
"tagline" : "You Know, for Search"
51+
}
52+
headers:
53+
Content-Length:
54+
- "547"
55+
Content-Type:
56+
- application/json
57+
X-Elastic-Product:
58+
- Elasticsearch
59+
status: 200 OK
60+
code: 200
61+
duration: 3.494911ms
62+
- id: 1
63+
request:
64+
proto: HTTP/1.1
65+
proto_major: 1
66+
proto_minor: 1
67+
content_length: 0
68+
transfer_encoding: []
69+
trailer: {}
70+
host: ""
71+
remote_addr: ""
72+
request_uri: ""
73+
body: ""
74+
form: {}
75+
headers:
76+
Authorization:
77+
- Basic ZWxhc3RpYzpjaGFuZ2VtZQ==
78+
User-Agent:
79+
- go-elasticsearch/7.17.10 (linux amd64; Go 1.23.4)
80+
X-Elastic-Client-Meta:
81+
- es=7.17.10,go=1.23.4,t=7.17.10,hc=1.23.4
82+
url: https://127.0.0.1:9200/
83+
method: GET
84+
response:
85+
proto: HTTP/1.1
86+
proto_major: 1
87+
proto_minor: 1
88+
transfer_encoding: []
89+
trailer: {}
90+
content_length: 547
91+
uncompressed: false
92+
body: |
93+
{
94+
"name" : "da1c4d2b1379",
95+
"cluster_name" : "elasticsearch",
96+
"cluster_uuid" : "KPcvXrW1Rnega2GT0sn3mg",
97+
"version" : {
98+
"number" : "9.0.0-SNAPSHOT",
99+
"build_flavor" : "default",
100+
"build_type" : "docker",
101+
"build_hash" : "873dc52360a9265824a70b3113c3dd350ff9249a",
102+
"build_date" : "2025-01-13T13:20:37.908330789Z",
103+
"build_snapshot" : true,
104+
"lucene_version" : "10.0.0",
105+
"minimum_wire_compatibility_version" : "8.18.0",
106+
"minimum_index_compatibility_version" : "8.0.0"
107+
},
108+
"tagline" : "You Know, for Search"
109+
}
110+
headers:
111+
Content-Length:
112+
- "547"
113+
Content-Type:
114+
- application/json
115+
X-Elastic-Product:
116+
- Elasticsearch
117+
status: 200 OK
118+
code: 200
119+
duration: 474.3µs

internal/fleetserver/client.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package fleetserver
6+
7+
import (
8+
"context"
9+
"crypto/tls"
10+
"fmt"
11+
"io"
12+
"net/http"
13+
"net/url"
14+
15+
"github.com/elastic/elastic-package/internal/certs"
16+
"github.com/elastic/elastic-package/internal/logger"
17+
)
18+
19+
// Client is a client for Fleet Server API. This API only supports authentication with API
20+
// keys, though some endpoints are also available without any authentication.
21+
type Client struct {
22+
address string
23+
apiKey string
24+
25+
certificateAuthority string
26+
tlSkipVerify bool
27+
28+
http *http.Client
29+
httpClientSetup func(*http.Client) *http.Client
30+
}
31+
32+
type ClientOption func(*Client)
33+
34+
func NewClient(address string, opts ...ClientOption) (*Client, error) {
35+
client := Client{
36+
address: address,
37+
}
38+
39+
for _, opt := range opts {
40+
opt(&client)
41+
}
42+
43+
httpClient, err := client.httpClient()
44+
if err != nil {
45+
return nil, fmt.Errorf("cannot create HTTP client: %w", err)
46+
}
47+
client.http = httpClient
48+
return &client, nil
49+
}
50+
51+
// APIKey option sets the API key to be used by the client for authentication.
52+
func APIKey(apiKey string) ClientOption {
53+
return func(c *Client) {
54+
c.apiKey = apiKey
55+
}
56+
}
57+
58+
// TLSSkipVerify option disables TLS verification.
59+
func TLSSkipVerify() ClientOption {
60+
return func(c *Client) {
61+
c.tlSkipVerify = true
62+
}
63+
}
64+
65+
// CertificateAuthority sets the certificate authority to be used by the client.
66+
func CertificateAuthority(certificateAuthority string) ClientOption {
67+
return func(c *Client) {
68+
c.certificateAuthority = certificateAuthority
69+
}
70+
}
71+
72+
// HTTPClientSetup adds an initializing function for the http client.
73+
func HTTPClientSetup(setup func(*http.Client) *http.Client) ClientOption {
74+
return func(c *Client) {
75+
c.httpClientSetup = setup
76+
}
77+
}
78+
79+
func (c *Client) httpClient() (*http.Client, error) {
80+
client := &http.Client{}
81+
if c.tlSkipVerify {
82+
client.Transport = &http.Transport{
83+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
84+
}
85+
} else if c.certificateAuthority != "" {
86+
rootCAs, err := certs.SystemPoolWithCACertificate(c.certificateAuthority)
87+
if err != nil {
88+
return nil, fmt.Errorf("reading CA certificate: %w", err)
89+
}
90+
client.Transport = &http.Transport{
91+
TLSClientConfig: &tls.Config{RootCAs: rootCAs},
92+
}
93+
}
94+
95+
if c.httpClientSetup != nil {
96+
client = c.httpClientSetup(client)
97+
}
98+
99+
return client, nil
100+
}
101+
102+
func (c *Client) httpRequest(ctx context.Context, method, resourcePath string, reqBody io.Reader) (*http.Request, error) {
103+
base, err := url.Parse(c.address)
104+
if err != nil {
105+
return nil, fmt.Errorf("could not create base URL from host: %v: %w", c.address, err)
106+
}
107+
108+
rel, err := url.Parse(resourcePath)
109+
if err != nil {
110+
return nil, fmt.Errorf("could not create relative URL from resource path: %v: %w", resourcePath, err)
111+
}
112+
113+
u := base.JoinPath(rel.EscapedPath())
114+
u.RawQuery = rel.RawQuery
115+
116+
logger.Debugf("%s %s", method, u)
117+
118+
req, err := http.NewRequestWithContext(ctx, method, u.String(), reqBody)
119+
if err != nil {
120+
return nil, fmt.Errorf("could not create %v request to Fleet Server API resource: %s: %w", method, resourcePath, err)
121+
}
122+
123+
if c.apiKey != "" {
124+
req.Header.Set("Authorization", "ApiKey "+c.apiKey)
125+
}
126+
127+
return req, nil
128+
}

internal/fleetserver/status.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package fleetserver
6+
7+
import (
8+
"context"
9+
"encoding/json"
10+
"fmt"
11+
"io"
12+
"net/url"
13+
14+
"github.com/elastic/elastic-package/internal/logger"
15+
)
16+
17+
type Status struct {
18+
Name string `json:"name"`
19+
Status string `json:"status"`
20+
21+
// Version is only present if client is authenticated.
22+
Version struct {
23+
Number string `json:"number"`
24+
} `json:"version"`
25+
}
26+
27+
func (c *Client) Status(ctx context.Context) (*Status, error) {
28+
statusURL, err := url.JoinPath(c.address, "/api/status")
29+
if err != nil {
30+
return nil, fmt.Errorf("could not build URL: %w", err)
31+
}
32+
logger.Debugf("GET %s", statusURL)
33+
req, err := c.httpRequest(ctx, "GET", statusURL, nil)
34+
if err != nil {
35+
return nil, err
36+
}
37+
resp, err := c.http.Do(req)
38+
if err != nil {
39+
return nil, fmt.Errorf("request failed (url: %s): %w", statusURL, err)
40+
}
41+
defer resp.Body.Close()
42+
if resp.StatusCode >= 300 {
43+
return nil, fmt.Errorf("unexpected status code %v", resp.StatusCode)
44+
}
45+
body, err := io.ReadAll(resp.Body)
46+
if err != nil {
47+
return nil, fmt.Errorf("failed to read response body: %w", err)
48+
}
49+
var status Status
50+
err = json.Unmarshal(body, &status)
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to parse response body: %w", err)
53+
}
54+
55+
return &status, nil
56+
}

0 commit comments

Comments
 (0)