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 }