internal/kibana/client.go (190 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package kibana
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"github.com/Masterminds/semver/v3"
"github.com/elastic/elastic-package/internal/certs"
"github.com/elastic/elastic-package/internal/install"
"github.com/elastic/elastic-package/internal/logger"
"github.com/elastic/elastic-package/internal/retry"
)
var (
ErrUndefinedHost = errors.New("missing kibana host")
ErrConflict = errors.New("resource already exists")
)
// Client is responsible for exporting dashboards from Kibana.
type Client struct {
host string
apiKey string
username string
password string
certificateAuthority string
tlSkipVerify bool
versionInfo VersionInfo
semver *semver.Version
retryMax int
http *http.Client
httpClientSetup func(*http.Client) *http.Client
}
// ClientOption is functional option modifying Kibana client.
type ClientOption func(*Client)
// NewClient creates a new instance of the client.
func NewClient(opts ...ClientOption) (*Client, error) {
c := &Client{
retryMax: 10,
}
for _, opt := range opts {
opt(c)
}
if c.host == "" {
return nil, ErrUndefinedHost
}
httpClient, err := c.newHttpClient()
if err != nil {
return nil, err
}
c.http = httpClient
// Allow to initialize version from tests.
var zeroVersion VersionInfo
if c.semver == nil || c.versionInfo == zeroVersion {
// Passing a nil context here because we are on initialization.
v, err := c.requestStatus(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get Kibana version: %w", err)
}
c.versionInfo = v.Version
// Version info may not contain any version if this is a managed Kibana.
if c.versionInfo.Number != "" {
c.semver, err = semver.NewVersion(c.versionInfo.Number)
if err != nil {
return nil, fmt.Errorf("failed to parse Kibana version (%s): %w", c.versionInfo.Number, err)
}
}
}
return c, nil
}
// Get client host
func (c *Client) Address() string {
return c.host
}
// Address option sets the host to use to connect to Kibana.
func Address(address string) ClientOption {
return func(c *Client) {
c.host = address
}
}
// APIKey option sets the API key to be used by the client for authentication.
func APIKey(apiKey string) ClientOption {
return func(c *Client) {
c.apiKey = apiKey
}
}
// TLSSkipVerify option disables TLS verification.
func TLSSkipVerify() ClientOption {
return func(c *Client) {
c.tlSkipVerify = true
}
}
// Username option sets the username to be used by the client.
func Username(username string) ClientOption {
return func(c *Client) {
c.username = username
}
}
// Password option sets the password to be used by the client.
func Password(password string) ClientOption {
return func(c *Client) {
c.password = password
}
}
// RetryMax configures the number of retries before failing.
func RetryMax(retryMax int) ClientOption {
return func(c *Client) {
c.retryMax = retryMax
}
}
// CertificateAuthority sets the certificate authority to be used by the client.
func CertificateAuthority(certificateAuthority string) ClientOption {
return func(c *Client) {
c.certificateAuthority = certificateAuthority
}
}
// HTTPClientSetup adds an initializing function for the http client.
func HTTPClientSetup(setup func(*http.Client) *http.Client) ClientOption {
return func(c *Client) {
c.httpClientSetup = setup
}
}
func (c *Client) get(ctx context.Context, resourcePath string) (int, []byte, error) {
return c.SendRequest(ctx, http.MethodGet, resourcePath, nil)
}
func (c *Client) post(ctx context.Context, resourcePath string, body []byte) (int, []byte, error) {
return c.SendRequest(ctx, http.MethodPost, resourcePath, body)
}
func (c *Client) put(ctx context.Context, resourcePath string, body []byte) (int, []byte, error) {
return c.SendRequest(ctx, http.MethodPut, resourcePath, body)
}
func (c *Client) delete(ctx context.Context, resourcePath string) (int, []byte, error) {
return c.SendRequest(ctx, http.MethodDelete, resourcePath, nil)
}
func (c *Client) SendRequest(ctx context.Context, method, resourcePath string, body []byte) (int, []byte, error) {
request, err := c.newRequest(ctx, method, resourcePath, bytes.NewReader(body))
if err != nil {
return 0, nil, err
}
return c.doRequest(request)
}
func (c *Client) newRequest(ctx context.Context, method, resourcePath string, reqBody io.Reader) (*http.Request, error) {
base, err := url.Parse(c.host)
if err != nil {
return nil, fmt.Errorf("could not create base URL from host: %v: %w", c.host, err)
}
rel, err := url.Parse(resourcePath)
if err != nil {
return nil, fmt.Errorf("could not create relative URL from resource path: %v: %w", resourcePath, err)
}
u := base.JoinPath(rel.EscapedPath())
u.RawQuery = rel.RawQuery
logger.Debugf("%s %s", method, u)
req, err := http.NewRequestWithContext(ctx, method, u.String(), reqBody)
if err != nil {
return nil, fmt.Errorf("could not create %v request to Kibana API resource: %s: %w", method, resourcePath, err)
}
if c.apiKey != "" {
req.Header.Set("Authorization", "ApiKey "+c.apiKey)
} else {
req.SetBasicAuth(c.username, c.password)
}
req.Header.Add("content-type", "application/json")
req.Header.Add("kbn-xsrf", install.DefaultStackVersion)
return req, nil
}
func (c *Client) doRequest(request *http.Request) (int, []byte, error) {
resp, err := c.http.Do(request)
if err != nil {
return 0, nil, fmt.Errorf("could not send request to Kibana API: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return resp.StatusCode, nil, fmt.Errorf("could not read response body: %w", err)
}
return resp.StatusCode, body, nil
}
func (c *Client) newHttpClient() (*http.Client, error) {
client := &http.Client{}
if c.tlSkipVerify {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
} else if c.certificateAuthority != "" {
rootCAs, err := certs.SystemPoolWithCACertificate(c.certificateAuthority)
if err != nil {
return nil, fmt.Errorf("reading CA certificate: %w", err)
}
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: rootCAs},
}
}
if c.retryMax > 0 {
opts := retry.HTTPOptions{
RetryMax: c.retryMax,
}
client = retry.WrapHTTPClient(client, opts)
}
if c.httpClientSetup != nil {
client = c.httpClientSetup(client)
}
return client, nil
}