feat: opensearch客户端初始化 feat: 索引模板接口 ai: 开发准则 chore: TDD流水线脚本

This commit is contained in:
mouseleee
2025-11-16 22:17:16 +08:00
commit da3883205c
18 changed files with 2707 additions and 0 deletions

135
client/client.go Normal file
View File

@@ -0,0 +1,135 @@
// 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
}

113
client/client_test.go Normal file
View File

@@ -0,0 +1,113 @@
package client
import (
"errors"
"testing"
"time"
)
func TestNewClient(t *testing.T) {
tests := []struct {
name string
config *Config
wantErr error
}{
{
name: "valid config",
config: &Config{
Endpoint: "https://example.com",
Region: "us-east-1",
AccessKey: "test-key",
SecretKey: "test-secret",
},
wantErr: nil,
},
{
name: "nil config",
config: nil,
wantErr: ErrInvalidConfig,
},
{
name: "missing endpoint",
config: &Config{
Region: "us-east-1",
AccessKey: "test-key",
SecretKey: "test-secret",
},
wantErr: ErrInvalidConfig,
},
{
name: "missing region",
config: &Config{
Endpoint: "https://example.com",
AccessKey: "test-key",
SecretKey: "test-secret",
},
wantErr: ErrInvalidConfig,
},
{
name: "missing access key",
config: &Config{
Endpoint: "https://example.com",
Region: "us-east-1",
SecretKey: "test-secret",
},
wantErr: ErrInvalidConfig,
},
{
name: "missing secret key",
config: &Config{
Endpoint: "https://example.com",
Region: "us-east-1",
AccessKey: "test-key",
},
wantErr: ErrInvalidConfig,
},
{
name: "with custom timeout",
config: &Config{
Endpoint: "https://example.com",
Region: "us-east-1",
AccessKey: "test-key",
SecretKey: "test-secret",
Timeout: 60 * time.Second,
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewClient(tt.config)
if !errors.Is(err, tt.wantErr) {
t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr == nil && got == nil {
t.Error("NewClient() returned nil client with no error")
}
if tt.wantErr != nil && got != nil {
t.Error("NewClient() returned client with error")
}
})
}
}
func TestClient_GetClient(t *testing.T) {
cfg := &Config{
Endpoint: "https://example.com",
Region: "us-east-1",
AccessKey: "test-key",
SecretKey: "test-secret",
}
client, err := NewClient(cfg)
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
osClient := client.GetClient()
if osClient == nil {
t.Error("GetClient() returned nil")
}
}