173 lines
5.6 KiB
Go
173 lines
5.6 KiB
Go
package nats
|
|
|
|
import (
|
|
"caatsm/internal/infra/config"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// ProvideNATSConn creates a reusable NATS connection with optional authentication.
|
|
func ProvideNATSConn(cfg *config.Config, logger *zap.Logger) (*nats.Conn, error) {
|
|
opts := []nats.Option{
|
|
nats.RetryOnFailedConnect(true),
|
|
nats.Timeout(cfg.Timeouts.Server),
|
|
nats.ReconnectWait(cfg.Timeouts.ReconnectWait),
|
|
// Use infinite reconnects so the app survives long NATS outages (e.g. docker compose down/up).
|
|
nats.MaxReconnects(-1),
|
|
nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
|
|
if err != nil {
|
|
logger.Warn("NATS disconnected", zap.Error(err))
|
|
}
|
|
}),
|
|
nats.ReconnectHandler(func(nc *nats.Conn) {
|
|
safeURL := sanitizeURLForLogging(nc.ConnectedUrl())
|
|
logger.Info("NATS reconnected", zap.String("url", safeURL))
|
|
}),
|
|
}
|
|
|
|
// Apply authentication options
|
|
authOpts, err := buildAuthOptions(&cfg.NATS.Auth, logger)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build auth options: %w", err)
|
|
}
|
|
opts = append(opts, authOpts...)
|
|
|
|
nc, err := nats.Connect(cfg.NATS.URL, opts...)
|
|
if err != nil {
|
|
safeURL := sanitizeURLForLogging(cfg.NATS.URL)
|
|
logger.Error("failed to connect to NATS",
|
|
zap.String("url", safeURL),
|
|
zap.Duration("timeout", cfg.Timeouts.Server),
|
|
zap.Duration("reconnect_wait", cfg.Timeouts.ReconnectWait),
|
|
zap.Error(err),
|
|
)
|
|
return nil, fmt.Errorf("failed to connect to NATS: %w", err)
|
|
}
|
|
|
|
return nc, nil
|
|
}
|
|
|
|
// buildAuthOptions builds NATS connection options based on authentication configuration.
|
|
func buildAuthOptions(auth *config.NATSAuthConfig, logger *zap.Logger) ([]nats.Option, error) {
|
|
var opts []nats.Option
|
|
authMethods := 0
|
|
|
|
// Token authentication (highest priority)
|
|
if auth.Token != "" {
|
|
authMethods++
|
|
logger.Debug("Using NATS token authentication")
|
|
opts = append(opts, nats.Token(auth.Token))
|
|
}
|
|
|
|
// Credentials file authentication
|
|
if auth.CredentialsFile != "" {
|
|
authMethods++
|
|
if authMethods > 1 {
|
|
return nil, fmt.Errorf("multiple authentication methods specified: only one of token, credentials_file, or user/password can be used")
|
|
}
|
|
logger.Debug("Using NATS credentials file authentication", zap.String("file", auth.CredentialsFile))
|
|
opts = append(opts, nats.UserCredentials(auth.CredentialsFile))
|
|
}
|
|
|
|
// User/Password authentication
|
|
if auth.User != "" || auth.Password != "" {
|
|
authMethods++
|
|
if authMethods > 1 {
|
|
return nil, fmt.Errorf("multiple authentication methods specified: only one of token, credentials_file, or user/password can be used")
|
|
}
|
|
if auth.User == "" || auth.Password == "" {
|
|
return nil, fmt.Errorf("both user and password must be specified for user/password authentication")
|
|
}
|
|
logger.Debug("Using NATS user/password authentication", zap.String("user", auth.User))
|
|
opts = append(opts, nats.UserInfo(auth.User, auth.Password))
|
|
}
|
|
|
|
// TLS configuration
|
|
if auth.TLSEnabled {
|
|
tlsConfig := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
|
|
// Load client certificate and key if provided
|
|
if auth.TLSCertFile != "" && auth.TLSKeyFile != "" {
|
|
cert, err := tls.LoadX509KeyPair(auth.TLSCertFile, auth.TLSKeyFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load TLS certificate: %w", err)
|
|
}
|
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
|
logger.Debug("Loaded TLS client certificate", zap.String("cert", auth.TLSCertFile))
|
|
}
|
|
|
|
// Load CA certificate for server verification if provided
|
|
if auth.TLSCAFile != "" {
|
|
caCert, err := os.ReadFile(auth.TLSCAFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read CA certificate file: %w", err)
|
|
}
|
|
caCertPool := x509.NewCertPool()
|
|
if !caCertPool.AppendCertsFromPEM(caCert) {
|
|
return nil, fmt.Errorf("failed to parse CA certificate from %s", auth.TLSCAFile)
|
|
}
|
|
tlsConfig.RootCAs = caCertPool
|
|
logger.Debug("Loaded TLS CA certificate", zap.String("ca_file", auth.TLSCAFile))
|
|
}
|
|
|
|
opts = append(opts, nats.Secure(tlsConfig))
|
|
logger.Debug("TLS enabled for NATS connection")
|
|
}
|
|
|
|
return opts, nil
|
|
}
|
|
|
|
// ProvideJetStream creates a NATS JetStream context using an existing connection.
|
|
// Returns nil, nil when cfg.NATS.Mode == "core" to support plain NATS servers without JetStream.
|
|
func ProvideJetStream(nc *nats.Conn, cfg *config.Config, logger *zap.Logger) (nats.JetStreamContext, error) {
|
|
mode := strings.ToLower(cfg.NATS.Mode)
|
|
if mode == "core" {
|
|
logger.Info("Skipping JetStream initialization for core NATS mode")
|
|
return nil, nil
|
|
}
|
|
|
|
// Get JetStream context
|
|
js, err := nc.JetStream()
|
|
if err != nil {
|
|
safeURL := sanitizeURLForLogging(cfg.NATS.URL)
|
|
logger.Error("failed to get JetStream context",
|
|
zap.String("url", safeURL),
|
|
zap.Error(err),
|
|
)
|
|
nc.Close()
|
|
return nil, fmt.Errorf("failed to get JetStream context: %w", err)
|
|
}
|
|
|
|
// Ensure the stream exists using StreamManager
|
|
streamName := cfg.NATS.Stream
|
|
consumerSubject := cfg.EffectiveSubscriptionTopic()
|
|
publisherSubject := strings.TrimSpace(cfg.Publisher.Topic)
|
|
|
|
streamSubjects := dedupeSubjects([]string{consumerSubject, publisherSubject})
|
|
if len(streamSubjects) == 0 {
|
|
logger.Error("no subjects configured for JetStream stream",
|
|
zap.String("stream", streamName),
|
|
zap.String("consumer_subject", consumerSubject),
|
|
zap.String("publisher_subject", publisherSubject),
|
|
)
|
|
nc.Close()
|
|
return nil, fmt.Errorf("no subjects configured for JetStream stream %s", streamName)
|
|
}
|
|
|
|
streamManager := NewStreamManagerWithConfig(js, streamName, streamSubjects, &cfg.NATS.StreamLimits, logger)
|
|
if err := streamManager.EnsureStream(); err != nil {
|
|
nc.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return js, nil
|
|
}
|