github.com/opensearch-project/opensearch-go/v2@v2.3.0/opensearch.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  //
     3  // The OpenSearch Contributors require contributions made to
     4  // this file be licensed under the Apache-2.0 license or a
     5  // compatible open source license.
     6  //
     7  // Modifications Copyright OpenSearch Contributors. See
     8  // GitHub history for details.
     9  
    10  // Licensed to Elasticsearch B.V. under one or more contributor
    11  // license agreements. See the NOTICE file distributed with
    12  // this work for additional information regarding copyright
    13  // ownership. Elasticsearch B.V. licenses this file to you under
    14  // the Apache License, Version 2.0 (the "License"); you may
    15  // not use this file except in compliance with the License.
    16  // You may obtain a copy of the License at
    17  //
    18  //    http://www.apache.org/licenses/LICENSE-2.0
    19  //
    20  // Unless required by applicable law or agreed to in writing,
    21  // software distributed under the License is distributed on an
    22  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    23  // KIND, either express or implied.  See the License for the
    24  // specific language governing permissions and limitations
    25  // under the License.
    26  
    27  package opensearch
    28  
    29  import (
    30  	"errors"
    31  	"fmt"
    32  	"net/http"
    33  	"net/url"
    34  	"os"
    35  	"regexp"
    36  	"strconv"
    37  	"strings"
    38  	"time"
    39  
    40  	"github.com/opensearch-project/opensearch-go/v2/signer"
    41  
    42  	"github.com/opensearch-project/opensearch-go/v2/internal/version"
    43  	"github.com/opensearch-project/opensearch-go/v2/opensearchapi"
    44  	"github.com/opensearch-project/opensearch-go/v2/opensearchtransport"
    45  )
    46  
    47  var (
    48  	reVersion *regexp.Regexp
    49  )
    50  
    51  func init() {
    52  	versionPattern := `^([0-9]+)\.([0-9]+)\.([0-9]+)`
    53  	reVersion = regexp.MustCompile(versionPattern)
    54  }
    55  
    56  const (
    57  	defaultURL          = "http://localhost:9200"
    58  	openSearch          = "opensearch"
    59  	unsupportedProduct  = "the client noticed that the server is not a supported distribution"
    60  	envOpenSearchURL    = "OPENSEARCH_URL"
    61  	envElasticsearchURL = "ELASTICSEARCH_URL"
    62  )
    63  
    64  // Version returns the package version as a string.
    65  //
    66  const Version = version.Client
    67  
    68  // Config represents the client configuration.
    69  //
    70  type Config struct {
    71  	Addresses []string // A list of nodes to use.
    72  	Username  string   // Username for HTTP Basic Authentication.
    73  	Password  string   // Password for HTTP Basic Authentication.
    74  
    75  	Header http.Header // Global HTTP request header.
    76  
    77  	Signer signer.Signer
    78  
    79  	// PEM-encoded certificate authorities.
    80  	// When set, an empty certificate pool will be created, and the certificates will be appended to it.
    81  	// The option is only valid when the transport is not specified, or when it's http.Transport.
    82  	CACert []byte
    83  
    84  	RetryOnStatus        []int // List of status codes for retry. Default: 502, 503, 504.
    85  	DisableRetry         bool  // Default: false.
    86  	EnableRetryOnTimeout bool  // Default: false.
    87  	MaxRetries           int   // Default: 3.
    88  
    89  	CompressRequestBody bool // Default: false.
    90  
    91  	DiscoverNodesOnStart  bool          // Discover nodes when initializing the client. Default: false.
    92  	DiscoverNodesInterval time.Duration // Discover nodes periodically. Default: disabled.
    93  
    94  	EnableMetrics     bool // Enable the metrics collection.
    95  	EnableDebugLogger bool // Enable the debug logging.
    96  
    97  	RetryBackoff func(attempt int) time.Duration // Optional backoff duration. Default: nil.
    98  
    99  	Transport http.RoundTripper            // The HTTP transport object.
   100  	Logger    opensearchtransport.Logger   // The logger object.
   101  	Selector  opensearchtransport.Selector // The selector object.
   102  
   103  	// Optional constructor function for a custom ConnectionPool. Default: nil.
   104  	ConnectionPoolFunc func([]*opensearchtransport.Connection, opensearchtransport.Selector) opensearchtransport.ConnectionPool
   105  }
   106  
   107  // Client represents the OpenSearch client.
   108  //
   109  type Client struct {
   110  	*opensearchapi.API   // Embeds the API methods
   111  	Transport            opensearchtransport.Interface
   112  }
   113  
   114  type esVersion struct {
   115  	Number       string `json:"number"`
   116  	BuildFlavor  string `json:"build_flavor"`
   117  	Distribution string `json:"distribution"`
   118  }
   119  
   120  type info struct {
   121  	Version esVersion `json:"version"`
   122  	Tagline string    `json:"tagline"`
   123  }
   124  
   125  // NewDefaultClient creates a new client with default options.
   126  //
   127  // It will use http://localhost:9200 as the default address.
   128  //
   129  // It will use the OPENSEARCH_URL/ELASTICSEARCH_URL environment variable, if set,
   130  // to configure the addresses; use a comma to separate multiple URLs.
   131  //
   132  // It's an error to set both OPENSEARCH_URL and ELASTICSEARCH_URL.
   133  //
   134  func NewDefaultClient() (*Client, error) {
   135  	return NewClient(Config{})
   136  }
   137  
   138  // NewClient creates a new client with configuration from cfg.
   139  //
   140  // It will use http://localhost:9200 as the default address.
   141  //
   142  // It will use the OPENSEARCH_URL/ELASTICSEARCH_URL environment variable, if set,
   143  // to configure the addresses; use a comma to separate multiple URLs.
   144  //
   145  // It's an error to set both OPENSEARCH_URL and ELASTICSEARCH_URL.
   146  //
   147  func NewClient(cfg Config) (*Client, error) {
   148  	var addrs []string
   149  
   150  	if len(cfg.Addresses) == 0 {
   151  		envAddress, err := getAddressFromEnvironment()
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  		addrs = envAddress
   156  	} else {
   157  		addrs = append(addrs, cfg.Addresses...)
   158  	}
   159  
   160  	urls, err := addrsToURLs(addrs)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("cannot create client: %s", err)
   163  	}
   164  
   165  	if len(urls) == 0 {
   166  		u, _ := url.Parse(defaultURL) // errcheck exclude
   167  		urls = append(urls, u)
   168  	}
   169  
   170  	// TODO: Refactor
   171  	if urls[0].User != nil {
   172  		cfg.Username = urls[0].User.Username()
   173  		pw, _ := urls[0].User.Password()
   174  		cfg.Password = pw
   175  	}
   176  
   177  	tp, err := opensearchtransport.New(opensearchtransport.Config{
   178  		URLs:     urls,
   179  		Username: cfg.Username,
   180  		Password: cfg.Password,
   181  
   182  		Header: cfg.Header,
   183  		CACert: cfg.CACert,
   184  
   185  		Signer: cfg.Signer,
   186  
   187  		RetryOnStatus:        cfg.RetryOnStatus,
   188  		DisableRetry:         cfg.DisableRetry,
   189  		EnableRetryOnTimeout: cfg.EnableRetryOnTimeout,
   190  		MaxRetries:           cfg.MaxRetries,
   191  		RetryBackoff:         cfg.RetryBackoff,
   192  
   193  		CompressRequestBody: cfg.CompressRequestBody,
   194  
   195  		EnableMetrics:     cfg.EnableMetrics,
   196  		EnableDebugLogger: cfg.EnableDebugLogger,
   197  
   198  		DiscoverNodesInterval: cfg.DiscoverNodesInterval,
   199  
   200  		Transport:          cfg.Transport,
   201  		Logger:             cfg.Logger,
   202  		Selector:           cfg.Selector,
   203  		ConnectionPoolFunc: cfg.ConnectionPoolFunc,
   204  	})
   205  	if err != nil {
   206  		return nil, fmt.Errorf("error creating transport: %s", err)
   207  	}
   208  
   209  	client := &Client{Transport: tp}
   210  	client.API = opensearchapi.New(client)
   211  
   212  	if cfg.DiscoverNodesOnStart {
   213  		go client.DiscoverNodes()
   214  	}
   215  
   216  	return client, err
   217  }
   218  
   219  func getAddressFromEnvironment() ([]string, error) {
   220  	fromOpenSearchEnv := addrsFromEnvironment(envOpenSearchURL)
   221  	fromElasticsearchEnv := addrsFromEnvironment(envElasticsearchURL)
   222  
   223  	if len(fromElasticsearchEnv) > 0 && len(fromOpenSearchEnv) > 0 {
   224  		return nil, fmt.Errorf("cannot create client: both %s and %s are set", envOpenSearchURL, envElasticsearchURL)
   225  	}
   226  	if len(fromOpenSearchEnv) > 0 {
   227  		return fromOpenSearchEnv, nil
   228  	}
   229  	return fromElasticsearchEnv, nil
   230  }
   231  
   232  // checkCompatibleInfo validates the information given by OpenSearch
   233  //
   234  func checkCompatibleInfo(info info) error {
   235  	major, _, _, err := ParseVersion(info.Version.Number)
   236  	if err != nil {
   237  		return err
   238  	}
   239  	if info.Version.Distribution == openSearch {
   240  		return nil
   241  	}
   242  	if major != 7 {
   243  		return errors.New(unsupportedProduct)
   244  	}
   245  	return nil
   246  }
   247  
   248  // ParseVersion returns an int64 representation of version.
   249  //
   250  func ParseVersion(version string) (int64, int64, int64, error) {
   251  	matches := reVersion.FindStringSubmatch(version)
   252  
   253  	if len(matches) < 4 {
   254  		return 0, 0, 0, fmt.Errorf("")
   255  	}
   256  	major, _ := strconv.ParseInt(matches[1], 10, 0)
   257  	minor, _ := strconv.ParseInt(matches[2], 10, 0)
   258  	patch, _ := strconv.ParseInt(matches[3], 10, 0)
   259  
   260  	return major, minor, patch, nil
   261  }
   262  
   263  // Perform delegates to Transport to execute a request and return a response.
   264  //
   265  func (c *Client) Perform(req *http.Request) (*http.Response, error) {
   266  	// Perform the original request.
   267  	return c.Transport.Perform(req)
   268  }
   269  
   270  // Metrics returns the client metrics.
   271  //
   272  func (c *Client) Metrics() (opensearchtransport.Metrics, error) {
   273  	if mt, ok := c.Transport.(opensearchtransport.Measurable); ok {
   274  		return mt.Metrics()
   275  	}
   276  	return opensearchtransport.Metrics{}, errors.New("transport is missing method Metrics()")
   277  }
   278  
   279  // DiscoverNodes reloads the client connections by fetching information from the cluster.
   280  //
   281  func (c *Client) DiscoverNodes() error {
   282  	if dt, ok := c.Transport.(opensearchtransport.Discoverable); ok {
   283  		return dt.DiscoverNodes()
   284  	}
   285  	return errors.New("transport is missing method DiscoverNodes()")
   286  }
   287  
   288  // addrsFromEnvironment returns a list of addresses by splitting
   289  // the given environment variable with comma, or an empty list.
   290  //
   291  func addrsFromEnvironment(name string) []string {
   292  	var addrs []string
   293  
   294  	if envURLs, ok := os.LookupEnv(name); ok && envURLs != "" {
   295  		list := strings.Split(envURLs, ",")
   296  		for _, u := range list {
   297  			addrs = append(addrs, strings.TrimSpace(u))
   298  		}
   299  	}
   300  
   301  	return addrs
   302  }
   303  
   304  // addrsToURLs creates a list of url.URL structures from url list.
   305  //
   306  func addrsToURLs(addrs []string) ([]*url.URL, error) {
   307  	var urls []*url.URL
   308  	for _, addr := range addrs {
   309  		u, err := url.Parse(strings.TrimRight(addr, "/"))
   310  		if err != nil {
   311  			return nil, fmt.Errorf("cannot parse url: %v", err)
   312  		}
   313  
   314  		urls = append(urls, u)
   315  	}
   316  	return urls, nil
   317  }