// Package client provides a high-level interface for interacting with AWS OpenSearch Service. // It handles connection management, authentication, and request signing. package client import ( "context" "crypto/tls" "errors" "fmt" "net/http" "time" "github.com/aws/aws-sdk-go-v2/aws" v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/opensearch-project/opensearch-go/v2" requestsigner "github.com/opensearch-project/opensearch-go/v2/signer/awsv2" ) var ( // ErrInvalidConfig is returned when the client configuration is invalid. ErrInvalidConfig = errors.New("invalid configuration") // ErrRequestFailed is returned when a request to OpenSearch fails. ErrRequestFailed = errors.New("request failed") ) const ( // DefaultTimeout is the default timeout for requests. DefaultTimeout = 30 * time.Second ) // Config holds the configuration for the OpenSearch client. type Config struct { // Endpoint is the OpenSearch cluster endpoint URL. Endpoint string // Region is the AWS region where the OpenSearch cluster is located. Region string // AccessKey is the AWS access key ID for authentication. AccessKey string // SecretKey is the AWS secret access key for authentication. SecretKey string // Timeout is the request timeout duration. Defaults to DefaultTimeout if not set. Timeout time.Duration } // Client represents an OpenSearch client that manages connections. type Client struct { config *Config client *opensearch.Client signer *v4.Signer } // NewClient creates a new OpenSearch client with the given configuration. // It returns an error if the configuration is invalid or if the client cannot be initialized. func NewClient(cfg *Config) (*Client, error) { if cfg == nil { return nil, fmt.Errorf("%w: config cannot be nil", ErrInvalidConfig) } if err := validateConfig(cfg); err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err) } // TODO: Use cfg.Timeout for client configuration // Currently timeout is not directly supported by opensearch-go client config // Create AWS credentials awsConfig := aws.Config{ Region: cfg.Region, Credentials: aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) { return aws.Credentials{ AccessKeyID: cfg.AccessKey, SecretAccessKey: cfg.SecretKey, }, nil }), } // Create AWS v4 signer signer := v4.NewSigner() // Create request signer for OpenSearch awsSigner, err := requestsigner.NewSignerWithService(awsConfig, "es") if err != nil { return nil, fmt.Errorf("failed to create request signer: %w", err) } // Configure OpenSearch client osConfig := opensearch.Config{ Addresses: []string{cfg.Endpoint}, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, }, }, Signer: awsSigner, } // Create OpenSearch client osClient, err := opensearch.NewClient(osConfig) if err != nil { return nil, fmt.Errorf("failed to create opensearch client: %w", err) } return &Client{ config: cfg, client: osClient, signer: signer, }, nil } // GetClient returns the underlying OpenSearch client for use by operation packages. func (c *Client) GetClient() *opensearch.Client { return c.client } // validateConfig validates the client configuration. func validateConfig(cfg *Config) error { if cfg.Endpoint == "" { return errors.New("endpoint is required") } if cfg.Region == "" { return errors.New("region is required") } if cfg.AccessKey == "" { return errors.New("access key is required") } if cfg.SecretKey == "" { return errors.New("secret key is required") } return nil }