feat: authsession service

This commit is contained in:
Ilia Denisov
2026-04-08 16:23:07 +02:00
committed by GitHub
parent 28f04916af
commit 86a68ed9d0
174 changed files with 31732 additions and 112 deletions
+610
View File
@@ -0,0 +1,610 @@
// Package config loads the authsession process configuration from environment
// variables.
package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"galaxy/authsession/internal/api/internalhttp"
"galaxy/authsession/internal/api/publichttp"
"go.uber.org/zap/zapcore"
)
const (
shutdownTimeoutEnvVar = "AUTHSESSION_SHUTDOWN_TIMEOUT"
logLevelEnvVar = "AUTHSESSION_LOG_LEVEL"
publicHTTPAddrEnvVar = "AUTHSESSION_PUBLIC_HTTP_ADDR"
publicHTTPReadHeaderTimeoutEnvVar = "AUTHSESSION_PUBLIC_HTTP_READ_HEADER_TIMEOUT"
publicHTTPReadTimeoutEnvVar = "AUTHSESSION_PUBLIC_HTTP_READ_TIMEOUT"
publicHTTPIdleTimeoutEnvVar = "AUTHSESSION_PUBLIC_HTTP_IDLE_TIMEOUT"
publicHTTPRequestTimeoutEnvVar = "AUTHSESSION_PUBLIC_HTTP_REQUEST_TIMEOUT"
internalHTTPAddrEnvVar = "AUTHSESSION_INTERNAL_HTTP_ADDR"
internalHTTPReadHeaderTimeoutEnvVar = "AUTHSESSION_INTERNAL_HTTP_READ_HEADER_TIMEOUT"
internalHTTPReadTimeoutEnvVar = "AUTHSESSION_INTERNAL_HTTP_READ_TIMEOUT"
internalHTTPIdleTimeoutEnvVar = "AUTHSESSION_INTERNAL_HTTP_IDLE_TIMEOUT"
internalHTTPRequestTimeoutEnvVar = "AUTHSESSION_INTERNAL_HTTP_REQUEST_TIMEOUT"
redisAddrEnvVar = "AUTHSESSION_REDIS_ADDR"
redisUsernameEnvVar = "AUTHSESSION_REDIS_USERNAME"
redisPasswordEnvVar = "AUTHSESSION_REDIS_PASSWORD"
redisDBEnvVar = "AUTHSESSION_REDIS_DB"
redisTLSEnabledEnvVar = "AUTHSESSION_REDIS_TLS_ENABLED"
redisOperationTimeoutEnvVar = "AUTHSESSION_REDIS_OPERATION_TIMEOUT"
redisChallengeKeyPrefixEnvVar = "AUTHSESSION_REDIS_CHALLENGE_KEY_PREFIX"
redisSessionKeyPrefixEnvVar = "AUTHSESSION_REDIS_SESSION_KEY_PREFIX"
redisUserSessionsKeyPrefixEnvVar = "AUTHSESSION_REDIS_USER_SESSIONS_KEY_PREFIX"
redisUserActiveSessionsKeyPrefixEnvVar = "AUTHSESSION_REDIS_USER_ACTIVE_SESSIONS_KEY_PREFIX"
redisSessionLimitKeyEnvVar = "AUTHSESSION_REDIS_SESSION_LIMIT_KEY"
redisGatewaySessionCacheKeyPrefixEnvVar = "AUTHSESSION_REDIS_GATEWAY_SESSION_CACHE_KEY_PREFIX"
redisGatewaySessionEventsStreamEnvVar = "AUTHSESSION_REDIS_GATEWAY_SESSION_EVENTS_STREAM"
redisGatewaySessionEventsStreamMaxLenEnvVar = "AUTHSESSION_REDIS_GATEWAY_SESSION_EVENTS_STREAM_MAX_LEN"
redisSendEmailCodeThrottleKeyPrefixEnvVar = "AUTHSESSION_REDIS_SEND_EMAIL_CODE_THROTTLE_KEY_PREFIX"
userServiceModeEnvVar = "AUTHSESSION_USER_SERVICE_MODE"
userServiceBaseURLEnvVar = "AUTHSESSION_USER_SERVICE_BASE_URL"
userServiceRequestTimeoutEnvVar = "AUTHSESSION_USER_SERVICE_REQUEST_TIMEOUT"
mailServiceModeEnvVar = "AUTHSESSION_MAIL_SERVICE_MODE"
mailServiceBaseURLEnvVar = "AUTHSESSION_MAIL_SERVICE_BASE_URL"
mailServiceRequestTimeoutEnvVar = "AUTHSESSION_MAIL_SERVICE_REQUEST_TIMEOUT"
otelServiceNameEnvVar = "OTEL_SERVICE_NAME"
otelTracesExporterEnvVar = "OTEL_TRACES_EXPORTER"
otelMetricsExporterEnvVar = "OTEL_METRICS_EXPORTER"
otelExporterOTLPProtocolEnvVar = "OTEL_EXPORTER_OTLP_PROTOCOL"
otelExporterOTLPTracesProtocolEnvVar = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
otelExporterOTLPMetricsProtocolEnvVar = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"
otelStdoutTracesEnabledEnvVar = "AUTHSESSION_OTEL_STDOUT_TRACES_ENABLED"
otelStdoutMetricsEnabledEnvVar = "AUTHSESSION_OTEL_STDOUT_METRICS_ENABLED"
defaultShutdownTimeout = 5 * time.Second
defaultLogLevel = "info"
defaultRedisDB = 0
defaultRedisOperationTimeout = 250 * time.Millisecond
defaultChallengeKeyPrefix = "authsession:challenge:"
defaultSessionKeyPrefix = "authsession:session:"
defaultUserSessionsKeyPrefix = "authsession:user-sessions:"
defaultUserActiveSessionsKeyPrefix = "authsession:user-active-sessions:"
defaultSessionLimitKey = "authsession:config:active-session-limit"
defaultGatewaySessionCacheKeyPrefix = "gateway:session:"
defaultGatewaySessionEventsStream = "gateway:session_events"
defaultGatewaySessionEventsStreamMaxLen = 1024
defaultSendEmailCodeThrottleKeyPrefix = "authsession:send-email-code-throttle:"
defaultUserServiceMode = userServiceModeStub
defaultUserServiceRequestTimeout = time.Second
defaultMailServiceMode = mailServiceModeStub
defaultMailServiceRequestTimeout = time.Second
defaultOTelServiceName = "galaxy-authsession"
otelExporterNone = "none"
otelExporterOTLP = "otlp"
otelProtocolHTTPProtobuf = "http/protobuf"
otelProtocolGRPC = "grpc"
userServiceModeStub = "stub"
userServiceModeREST = "rest"
mailServiceModeStub = "stub"
mailServiceModeREST = "rest"
)
// Config stores the full process-level authsession configuration.
type Config struct {
// ShutdownTimeout bounds graceful shutdown of every long-lived component.
ShutdownTimeout time.Duration
// Logging configures the process-wide structured logger.
Logging LoggingConfig
// PublicHTTP configures the public HTTP listener.
PublicHTTP publichttp.Config
// InternalHTTP configures the trusted internal HTTP listener.
InternalHTTP internalhttp.Config
// Redis configures the Redis-backed adapters.
Redis RedisConfig
// UserService configures the selectable runtime user-directory adapter.
UserService UserServiceConfig
// MailService configures the selectable runtime mail-delivery adapter.
MailService MailServiceConfig
// Telemetry configures the process-wide OpenTelemetry runtime.
Telemetry TelemetryConfig
}
// LoggingConfig configures the process-wide structured logger.
type LoggingConfig struct {
// Level stores the zap-compatible log level string.
Level string
}
// RedisConfig configures the Redis-backed authsession adapters.
type RedisConfig struct {
// Addr is the shared Redis address used by the authsession adapters.
Addr string
// Username is the optional Redis ACL username.
Username string
// Password is the optional Redis ACL password.
Password string
// DB is the Redis logical database index.
DB int
// TLSEnabled configures whether Redis connections use TLS.
TLSEnabled bool
// OperationTimeout bounds each adapter Redis round trip.
OperationTimeout time.Duration
// ChallengeKeyPrefix namespaces the challenge source-of-truth records.
ChallengeKeyPrefix string
// SessionKeyPrefix namespaces the primary session records.
SessionKeyPrefix string
// UserSessionsKeyPrefix namespaces the all-session user index.
UserSessionsKeyPrefix string
// UserActiveSessionsKeyPrefix namespaces the active-session user index.
UserActiveSessionsKeyPrefix string
// SessionLimitKey stores the exact session-limit Redis key.
SessionLimitKey string
// GatewaySessionCacheKeyPrefix namespaces the projected gateway session
// cache keys.
GatewaySessionCacheKeyPrefix string
// GatewaySessionEventsStream stores the projected gateway session-events
// Redis Stream key.
GatewaySessionEventsStream string
// GatewaySessionEventsStreamMaxLen bounds the projected gateway session
// event stream with approximate trimming.
GatewaySessionEventsStreamMaxLen int64
// SendEmailCodeThrottleKeyPrefix namespaces the resend-throttle TTL keys.
SendEmailCodeThrottleKeyPrefix string
}
// UserServiceConfig configures the runtime user-directory integration mode.
type UserServiceConfig struct {
// Mode selects the runtime adapter implementation. Supported values are
// `stub` and `rest`.
Mode string
// BaseURL is the absolute base URL of the REST-backed user-service when
// Mode is `rest`.
BaseURL string
// RequestTimeout bounds each outbound user-service request when Mode is
// `rest`.
RequestTimeout time.Duration
}
// MailServiceConfig configures the runtime mail-delivery integration mode.
type MailServiceConfig struct {
// Mode selects the runtime adapter implementation. Supported values are
// `stub` and `rest`.
Mode string
// BaseURL is the absolute base URL of the REST-backed mail service when
// Mode is `rest`.
BaseURL string
// RequestTimeout bounds each outbound mail-service request when Mode is
// `rest`.
RequestTimeout time.Duration
}
// TelemetryConfig configures the authsession OpenTelemetry runtime.
type TelemetryConfig struct {
// ServiceName overrides the default OpenTelemetry service name.
ServiceName string
// TracesExporter selects the external traces exporter. Supported values are
// `none` and `otlp`.
TracesExporter string
// MetricsExporter selects the external metrics exporter. Supported values
// are `none` and `otlp`.
MetricsExporter string
// TracesProtocol selects the OTLP traces protocol when TracesExporter is
// `otlp`.
TracesProtocol string
// MetricsProtocol selects the OTLP metrics protocol when MetricsExporter is
// `otlp`.
MetricsProtocol string
// StdoutTracesEnabled enables the additional stdout trace exporter used for
// local development and debugging.
StdoutTracesEnabled bool
// StdoutMetricsEnabled enables the additional stdout metric exporter used
// for local development and debugging.
StdoutMetricsEnabled bool
}
// DefaultConfig returns the default authsession process configuration with all
// optional values filled.
func DefaultConfig() Config {
return Config{
ShutdownTimeout: defaultShutdownTimeout,
Logging: LoggingConfig{
Level: defaultLogLevel,
},
PublicHTTP: publichttp.DefaultConfig(),
InternalHTTP: internalhttp.DefaultConfig(),
Redis: RedisConfig{
DB: defaultRedisDB,
OperationTimeout: defaultRedisOperationTimeout,
ChallengeKeyPrefix: defaultChallengeKeyPrefix,
SessionKeyPrefix: defaultSessionKeyPrefix,
UserSessionsKeyPrefix: defaultUserSessionsKeyPrefix,
UserActiveSessionsKeyPrefix: defaultUserActiveSessionsKeyPrefix,
SessionLimitKey: defaultSessionLimitKey,
GatewaySessionCacheKeyPrefix: defaultGatewaySessionCacheKeyPrefix,
GatewaySessionEventsStream: defaultGatewaySessionEventsStream,
GatewaySessionEventsStreamMaxLen: defaultGatewaySessionEventsStreamMaxLen,
SendEmailCodeThrottleKeyPrefix: defaultSendEmailCodeThrottleKeyPrefix,
},
UserService: UserServiceConfig{
Mode: defaultUserServiceMode,
RequestTimeout: defaultUserServiceRequestTimeout,
},
MailService: MailServiceConfig{
Mode: defaultMailServiceMode,
RequestTimeout: defaultMailServiceRequestTimeout,
},
Telemetry: TelemetryConfig{
ServiceName: defaultOTelServiceName,
TracesExporter: otelExporterNone,
MetricsExporter: otelExporterNone,
},
}
}
// LoadFromEnv loads the authsession process configuration from environment
// variables, applying documented defaults where appropriate.
func LoadFromEnv() (Config, error) {
cfg := DefaultConfig()
var err error
cfg.ShutdownTimeout, err = loadDurationEnvWithDefault(shutdownTimeoutEnvVar, cfg.ShutdownTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.Logging.Level = loadStringEnvWithDefault(logLevelEnvVar, cfg.Logging.Level)
if err := validateLogLevel(cfg.Logging.Level); err != nil {
return Config{}, fmt.Errorf("load authsession config: %s: %w", logLevelEnvVar, err)
}
cfg.PublicHTTP.Addr = loadStringEnvWithDefault(publicHTTPAddrEnvVar, cfg.PublicHTTP.Addr)
cfg.PublicHTTP.ReadHeaderTimeout, err = loadDurationEnvWithDefault(publicHTTPReadHeaderTimeoutEnvVar, cfg.PublicHTTP.ReadHeaderTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.PublicHTTP.ReadTimeout, err = loadDurationEnvWithDefault(publicHTTPReadTimeoutEnvVar, cfg.PublicHTTP.ReadTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.PublicHTTP.IdleTimeout, err = loadDurationEnvWithDefault(publicHTTPIdleTimeoutEnvVar, cfg.PublicHTTP.IdleTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.PublicHTTP.RequestTimeout, err = loadDurationEnvWithDefault(publicHTTPRequestTimeoutEnvVar, cfg.PublicHTTP.RequestTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.InternalHTTP.Addr = loadStringEnvWithDefault(internalHTTPAddrEnvVar, cfg.InternalHTTP.Addr)
cfg.InternalHTTP.ReadHeaderTimeout, err = loadDurationEnvWithDefault(internalHTTPReadHeaderTimeoutEnvVar, cfg.InternalHTTP.ReadHeaderTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.InternalHTTP.ReadTimeout, err = loadDurationEnvWithDefault(internalHTTPReadTimeoutEnvVar, cfg.InternalHTTP.ReadTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.InternalHTTP.IdleTimeout, err = loadDurationEnvWithDefault(internalHTTPIdleTimeoutEnvVar, cfg.InternalHTTP.IdleTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.InternalHTTP.RequestTimeout, err = loadDurationEnvWithDefault(internalHTTPRequestTimeoutEnvVar, cfg.InternalHTTP.RequestTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.Redis.Addr = loadStringEnvWithDefault(redisAddrEnvVar, cfg.Redis.Addr)
cfg.Redis.Username = os.Getenv(redisUsernameEnvVar)
cfg.Redis.Password = os.Getenv(redisPasswordEnvVar)
cfg.Redis.DB, err = loadIntEnvWithDefault(redisDBEnvVar, cfg.Redis.DB)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.Redis.TLSEnabled, err = loadBoolEnvWithDefault(redisTLSEnabledEnvVar, cfg.Redis.TLSEnabled)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.Redis.OperationTimeout, err = loadDurationEnvWithDefault(redisOperationTimeoutEnvVar, cfg.Redis.OperationTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.Redis.ChallengeKeyPrefix = loadStringEnvWithDefault(redisChallengeKeyPrefixEnvVar, cfg.Redis.ChallengeKeyPrefix)
cfg.Redis.SessionKeyPrefix = loadStringEnvWithDefault(redisSessionKeyPrefixEnvVar, cfg.Redis.SessionKeyPrefix)
cfg.Redis.UserSessionsKeyPrefix = loadStringEnvWithDefault(redisUserSessionsKeyPrefixEnvVar, cfg.Redis.UserSessionsKeyPrefix)
cfg.Redis.UserActiveSessionsKeyPrefix = loadStringEnvWithDefault(redisUserActiveSessionsKeyPrefixEnvVar, cfg.Redis.UserActiveSessionsKeyPrefix)
cfg.Redis.SessionLimitKey = loadStringEnvWithDefault(redisSessionLimitKeyEnvVar, cfg.Redis.SessionLimitKey)
cfg.Redis.GatewaySessionCacheKeyPrefix = loadStringEnvWithDefault(redisGatewaySessionCacheKeyPrefixEnvVar, cfg.Redis.GatewaySessionCacheKeyPrefix)
cfg.Redis.GatewaySessionEventsStream = loadStringEnvWithDefault(redisGatewaySessionEventsStreamEnvVar, cfg.Redis.GatewaySessionEventsStream)
streamMaxLen, err := loadInt64EnvWithDefault(redisGatewaySessionEventsStreamMaxLenEnvVar, cfg.Redis.GatewaySessionEventsStreamMaxLen)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.Redis.GatewaySessionEventsStreamMaxLen = streamMaxLen
cfg.Redis.SendEmailCodeThrottleKeyPrefix = loadStringEnvWithDefault(redisSendEmailCodeThrottleKeyPrefixEnvVar, cfg.Redis.SendEmailCodeThrottleKeyPrefix)
cfg.UserService.Mode = strings.TrimSpace(loadStringEnvWithDefault(userServiceModeEnvVar, cfg.UserService.Mode))
cfg.UserService.BaseURL = loadStringEnvWithDefault(userServiceBaseURLEnvVar, cfg.UserService.BaseURL)
cfg.UserService.RequestTimeout, err = loadDurationEnvWithDefault(userServiceRequestTimeoutEnvVar, cfg.UserService.RequestTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.MailService.Mode = strings.TrimSpace(loadStringEnvWithDefault(mailServiceModeEnvVar, cfg.MailService.Mode))
cfg.MailService.BaseURL = loadStringEnvWithDefault(mailServiceBaseURLEnvVar, cfg.MailService.BaseURL)
cfg.MailService.RequestTimeout, err = loadDurationEnvWithDefault(mailServiceRequestTimeoutEnvVar, cfg.MailService.RequestTimeout)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.Telemetry.ServiceName = loadStringEnvWithDefault(otelServiceNameEnvVar, cfg.Telemetry.ServiceName)
cfg.Telemetry.TracesExporter = normalizeExporterValue(loadStringEnvWithDefault(otelTracesExporterEnvVar, cfg.Telemetry.TracesExporter))
cfg.Telemetry.MetricsExporter = normalizeExporterValue(loadStringEnvWithDefault(otelMetricsExporterEnvVar, cfg.Telemetry.MetricsExporter))
cfg.Telemetry.TracesProtocol = loadOTLPProtocol(
os.Getenv(otelExporterOTLPTracesProtocolEnvVar),
os.Getenv(otelExporterOTLPProtocolEnvVar),
cfg.Telemetry.TracesExporter,
)
cfg.Telemetry.MetricsProtocol = loadOTLPProtocol(
os.Getenv(otelExporterOTLPMetricsProtocolEnvVar),
os.Getenv(otelExporterOTLPProtocolEnvVar),
cfg.Telemetry.MetricsExporter,
)
cfg.Telemetry.StdoutTracesEnabled, err = loadBoolEnvWithDefault(otelStdoutTracesEnabledEnvVar, cfg.Telemetry.StdoutTracesEnabled)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
cfg.Telemetry.StdoutMetricsEnabled, err = loadBoolEnvWithDefault(otelStdoutMetricsEnabledEnvVar, cfg.Telemetry.StdoutMetricsEnabled)
if err != nil {
return Config{}, fmt.Errorf("load authsession config: %w", err)
}
if err := cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
}
// Validate reports whether cfg contains a consistent authsession process
// configuration.
func (cfg Config) Validate() error {
switch {
case cfg.ShutdownTimeout <= 0:
return fmt.Errorf("load authsession config: %s must be positive", shutdownTimeoutEnvVar)
case strings.TrimSpace(cfg.Redis.Addr) == "":
return fmt.Errorf("load authsession config: %s must not be empty", redisAddrEnvVar)
case cfg.Redis.DB < 0:
return fmt.Errorf("load authsession config: %s must not be negative", redisDBEnvVar)
case cfg.Redis.OperationTimeout <= 0:
return fmt.Errorf("load authsession config: %s must be positive", redisOperationTimeoutEnvVar)
case strings.TrimSpace(cfg.Redis.ChallengeKeyPrefix) == "":
return fmt.Errorf("load authsession config: %s must not be empty", redisChallengeKeyPrefixEnvVar)
case strings.TrimSpace(cfg.Redis.SessionKeyPrefix) == "":
return fmt.Errorf("load authsession config: %s must not be empty", redisSessionKeyPrefixEnvVar)
case strings.TrimSpace(cfg.Redis.UserSessionsKeyPrefix) == "":
return fmt.Errorf("load authsession config: %s must not be empty", redisUserSessionsKeyPrefixEnvVar)
case strings.TrimSpace(cfg.Redis.UserActiveSessionsKeyPrefix) == "":
return fmt.Errorf("load authsession config: %s must not be empty", redisUserActiveSessionsKeyPrefixEnvVar)
case strings.TrimSpace(cfg.Redis.SessionLimitKey) == "":
return fmt.Errorf("load authsession config: %s must not be empty", redisSessionLimitKeyEnvVar)
case strings.TrimSpace(cfg.Redis.GatewaySessionCacheKeyPrefix) == "":
return fmt.Errorf("load authsession config: %s must not be empty", redisGatewaySessionCacheKeyPrefixEnvVar)
case strings.TrimSpace(cfg.Redis.GatewaySessionEventsStream) == "":
return fmt.Errorf("load authsession config: %s must not be empty", redisGatewaySessionEventsStreamEnvVar)
case cfg.Redis.GatewaySessionEventsStreamMaxLen <= 0:
return fmt.Errorf("load authsession config: %s must be positive", redisGatewaySessionEventsStreamMaxLenEnvVar)
case strings.TrimSpace(cfg.Redis.SendEmailCodeThrottleKeyPrefix) == "":
return fmt.Errorf("load authsession config: %s must not be empty", redisSendEmailCodeThrottleKeyPrefixEnvVar)
}
if err := cfg.PublicHTTP.Validate(); err != nil {
return fmt.Errorf("load authsession config: public HTTP: %w", err)
}
if err := cfg.InternalHTTP.Validate(); err != nil {
return fmt.Errorf("load authsession config: internal HTTP: %w", err)
}
if err := cfg.UserService.Validate(); err != nil {
return fmt.Errorf("load authsession config: %w", err)
}
if err := cfg.MailService.Validate(); err != nil {
return fmt.Errorf("load authsession config: %w", err)
}
if err := cfg.Telemetry.Validate(); err != nil {
return fmt.Errorf("load authsession config: %w", err)
}
return nil
}
// Validate reports whether cfg contains a supported user-service runtime
// configuration.
func (cfg UserServiceConfig) Validate() error {
switch cfg.Mode {
case userServiceModeStub:
return nil
case userServiceModeREST:
if strings.TrimSpace(cfg.BaseURL) == "" {
return fmt.Errorf("%s must not be empty in rest mode", userServiceBaseURLEnvVar)
}
if cfg.RequestTimeout <= 0 {
return fmt.Errorf("%s must be positive in rest mode", userServiceRequestTimeoutEnvVar)
}
return nil
default:
return fmt.Errorf("%s %q is unsupported", userServiceModeEnvVar, cfg.Mode)
}
}
// Validate reports whether cfg contains a supported mail-service runtime
// configuration.
func (cfg MailServiceConfig) Validate() error {
switch cfg.Mode {
case mailServiceModeStub:
return nil
case mailServiceModeREST:
if strings.TrimSpace(cfg.BaseURL) == "" {
return fmt.Errorf("%s must not be empty in rest mode", mailServiceBaseURLEnvVar)
}
if cfg.RequestTimeout <= 0 {
return fmt.Errorf("%s must be positive in rest mode", mailServiceRequestTimeoutEnvVar)
}
return nil
default:
return fmt.Errorf("%s %q is unsupported", mailServiceModeEnvVar, cfg.Mode)
}
}
// Validate reports whether cfg contains a supported OpenTelemetry exporter
// configuration.
func (cfg TelemetryConfig) Validate() error {
switch cfg.TracesExporter {
case otelExporterNone, otelExporterOTLP:
default:
return fmt.Errorf("%s %q is unsupported", otelTracesExporterEnvVar, cfg.TracesExporter)
}
switch cfg.MetricsExporter {
case otelExporterNone, otelExporterOTLP:
default:
return fmt.Errorf("%s %q is unsupported", otelMetricsExporterEnvVar, cfg.MetricsExporter)
}
if cfg.TracesProtocol != "" && cfg.TracesProtocol != otelProtocolHTTPProtobuf && cfg.TracesProtocol != otelProtocolGRPC {
return fmt.Errorf("%s %q is unsupported", otelExporterOTLPTracesProtocolEnvVar, cfg.TracesProtocol)
}
if cfg.MetricsProtocol != "" && cfg.MetricsProtocol != otelProtocolHTTPProtobuf && cfg.MetricsProtocol != otelProtocolGRPC {
return fmt.Errorf("%s %q is unsupported", otelExporterOTLPMetricsProtocolEnvVar, cfg.MetricsProtocol)
}
return nil
}
func loadStringEnvWithDefault(name string, value string) string {
if raw, ok := os.LookupEnv(name); ok {
return strings.TrimSpace(raw)
}
return value
}
func loadDurationEnvWithDefault(name string, value time.Duration) (time.Duration, error) {
raw, ok := os.LookupEnv(name)
if !ok {
return value, nil
}
parsed, err := time.ParseDuration(strings.TrimSpace(raw))
if err != nil {
return 0, fmt.Errorf("%s: %w", name, err)
}
return parsed, nil
}
func loadIntEnvWithDefault(name string, value int) (int, error) {
raw, ok := os.LookupEnv(name)
if !ok {
return value, nil
}
parsed, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil {
return 0, fmt.Errorf("%s: %w", name, err)
}
return parsed, nil
}
func loadInt64EnvWithDefault(name string, value int64) (int64, error) {
raw, ok := os.LookupEnv(name)
if !ok {
return value, nil
}
parsed, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err != nil {
return 0, fmt.Errorf("%s: %w", name, err)
}
return parsed, nil
}
func loadBoolEnvWithDefault(name string, value bool) (bool, error) {
raw, ok := os.LookupEnv(name)
if !ok {
return value, nil
}
parsed, err := strconv.ParseBool(strings.TrimSpace(raw))
if err != nil {
return false, fmt.Errorf("%s: %w", name, err)
}
return parsed, nil
}
func validateLogLevel(value string) error {
var level zapcore.Level
if err := level.UnmarshalText([]byte(strings.TrimSpace(value))); err != nil {
return err
}
return nil
}
func normalizeExporterValue(value string) string {
switch strings.TrimSpace(value) {
case "", otelExporterNone:
return otelExporterNone
default:
return strings.TrimSpace(value)
}
}
func loadOTLPProtocol(primary string, fallback string, exporter string) string {
protocol := strings.TrimSpace(primary)
if protocol == "" {
protocol = strings.TrimSpace(fallback)
}
if protocol == "" && exporter == otelExporterOTLP {
return otelProtocolHTTPProtobuf
}
return protocol
}
+161
View File
@@ -0,0 +1,161 @@
package config
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadFromEnvUsesDefaults(t *testing.T) {
t.Setenv(redisAddrEnvVar, "127.0.0.1:6379")
cfg, err := LoadFromEnv()
require.NoError(t, err)
defaults := DefaultConfig()
assert.Equal(t, defaults.ShutdownTimeout, cfg.ShutdownTimeout)
assert.Equal(t, defaults.Logging.Level, cfg.Logging.Level)
assert.Equal(t, defaults.PublicHTTP, cfg.PublicHTTP)
assert.Equal(t, defaults.InternalHTTP, cfg.InternalHTTP)
assert.Equal(t, "127.0.0.1:6379", cfg.Redis.Addr)
assert.Equal(t, defaults.Redis.DB, cfg.Redis.DB)
assert.Equal(t, defaults.Redis.OperationTimeout, cfg.Redis.OperationTimeout)
assert.Equal(t, defaults.UserService, cfg.UserService)
assert.Equal(t, defaults.MailService, cfg.MailService)
assert.Equal(t, defaults.Telemetry.ServiceName, cfg.Telemetry.ServiceName)
assert.Equal(t, defaults.Telemetry.TracesExporter, cfg.Telemetry.TracesExporter)
assert.Equal(t, defaults.Telemetry.MetricsExporter, cfg.Telemetry.MetricsExporter)
assert.False(t, cfg.Telemetry.StdoutTracesEnabled)
assert.False(t, cfg.Telemetry.StdoutMetricsEnabled)
}
func TestLoadFromEnvAppliesOverrides(t *testing.T) {
t.Setenv(shutdownTimeoutEnvVar, "9s")
t.Setenv(logLevelEnvVar, "debug")
t.Setenv(publicHTTPAddrEnvVar, "127.0.0.1:18080")
t.Setenv(internalHTTPAddrEnvVar, "127.0.0.1:18081")
t.Setenv(redisAddrEnvVar, "127.0.0.1:6380")
t.Setenv(redisUsernameEnvVar, "alice")
t.Setenv(redisPasswordEnvVar, "secret")
t.Setenv(redisDBEnvVar, "3")
t.Setenv(redisTLSEnabledEnvVar, "true")
t.Setenv(redisOperationTimeoutEnvVar, "750ms")
t.Setenv(userServiceModeEnvVar, "rest")
t.Setenv(userServiceBaseURLEnvVar, "http://127.0.0.1:19090")
t.Setenv(userServiceRequestTimeoutEnvVar, "900ms")
t.Setenv(mailServiceModeEnvVar, "rest")
t.Setenv(mailServiceBaseURLEnvVar, "http://127.0.0.1:19091")
t.Setenv(mailServiceRequestTimeoutEnvVar, "950ms")
t.Setenv(otelServiceNameEnvVar, "custom-authsession")
t.Setenv(otelTracesExporterEnvVar, "otlp")
t.Setenv(otelMetricsExporterEnvVar, "otlp")
t.Setenv(otelExporterOTLPProtocolEnvVar, "grpc")
t.Setenv(otelStdoutTracesEnabledEnvVar, "true")
t.Setenv(otelStdoutMetricsEnabledEnvVar, "true")
cfg, err := LoadFromEnv()
require.NoError(t, err)
assert.Equal(t, 9*time.Second, cfg.ShutdownTimeout)
assert.Equal(t, "debug", cfg.Logging.Level)
assert.Equal(t, "127.0.0.1:18080", cfg.PublicHTTP.Addr)
assert.Equal(t, "127.0.0.1:18081", cfg.InternalHTTP.Addr)
assert.Equal(t, "127.0.0.1:6380", cfg.Redis.Addr)
assert.Equal(t, "alice", cfg.Redis.Username)
assert.Equal(t, "secret", cfg.Redis.Password)
assert.Equal(t, 3, cfg.Redis.DB)
assert.True(t, cfg.Redis.TLSEnabled)
assert.Equal(t, 750*time.Millisecond, cfg.Redis.OperationTimeout)
assert.Equal(t, UserServiceConfig{
Mode: "rest",
BaseURL: "http://127.0.0.1:19090",
RequestTimeout: 900 * time.Millisecond,
}, cfg.UserService)
assert.Equal(t, MailServiceConfig{
Mode: "rest",
BaseURL: "http://127.0.0.1:19091",
RequestTimeout: 950 * time.Millisecond,
}, cfg.MailService)
assert.Equal(t, "custom-authsession", cfg.Telemetry.ServiceName)
assert.Equal(t, "otlp", cfg.Telemetry.TracesExporter)
assert.Equal(t, "otlp", cfg.Telemetry.MetricsExporter)
assert.Equal(t, "grpc", cfg.Telemetry.TracesProtocol)
assert.Equal(t, "grpc", cfg.Telemetry.MetricsProtocol)
assert.True(t, cfg.Telemetry.StdoutTracesEnabled)
assert.True(t, cfg.Telemetry.StdoutMetricsEnabled)
}
func TestLoadFromEnvRejectsInvalidValues(t *testing.T) {
tests := []struct {
name string
envName string
envVal string
}{
{name: "invalid duration", envName: shutdownTimeoutEnvVar, envVal: "later"},
{name: "invalid bool", envName: otelStdoutTracesEnabledEnvVar, envVal: "sometimes"},
{name: "invalid log level", envName: logLevelEnvVar, envVal: "verbose"},
{name: "invalid traces protocol", envName: otelExporterOTLPTracesProtocolEnvVar, envVal: "udp"},
{name: "invalid user service mode", envName: userServiceModeEnvVar, envVal: "grpc"},
{name: "invalid user service timeout", envName: userServiceRequestTimeoutEnvVar, envVal: "never"},
{name: "invalid mail service mode", envName: mailServiceModeEnvVar, envVal: "grpc"},
{name: "invalid mail service timeout", envName: mailServiceRequestTimeoutEnvVar, envVal: "never"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Setenv(redisAddrEnvVar, "127.0.0.1:6379")
t.Setenv(tt.envName, tt.envVal)
if tt.envName == otelExporterOTLPTracesProtocolEnvVar {
t.Setenv(otelTracesExporterEnvVar, "otlp")
}
_, err := LoadFromEnv()
require.Error(t, err)
assert.Contains(t, err.Error(), tt.envName)
})
}
}
func TestLoadFromEnvRejectsInvalidRESTUserServiceConfiguration(t *testing.T) {
t.Setenv(redisAddrEnvVar, "127.0.0.1:6379")
t.Setenv(userServiceModeEnvVar, "rest")
t.Run("missing base url", func(t *testing.T) {
_, err := LoadFromEnv()
require.Error(t, err)
assert.Contains(t, err.Error(), userServiceBaseURLEnvVar)
})
t.Run("non positive timeout", func(t *testing.T) {
t.Setenv(userServiceBaseURLEnvVar, "http://127.0.0.1:19090")
t.Setenv(userServiceRequestTimeoutEnvVar, "0s")
_, err := LoadFromEnv()
require.Error(t, err)
assert.Contains(t, err.Error(), userServiceRequestTimeoutEnvVar)
})
}
func TestLoadFromEnvRejectsInvalidRESTMailServiceConfiguration(t *testing.T) {
t.Setenv(redisAddrEnvVar, "127.0.0.1:6379")
t.Setenv(mailServiceModeEnvVar, "rest")
t.Run("missing base url", func(t *testing.T) {
_, err := LoadFromEnv()
require.Error(t, err)
assert.Contains(t, err.Error(), mailServiceBaseURLEnvVar)
})
t.Run("non positive timeout", func(t *testing.T) {
t.Setenv(mailServiceBaseURLEnvVar, "http://127.0.0.1:19091")
t.Setenv(mailServiceRequestTimeoutEnvVar, "0s")
_, err := LoadFromEnv()
require.Error(t, err)
assert.Contains(t, err.Error(), mailServiceRequestTimeoutEnvVar)
})
}