From 3d905757f6f4f4ac000e9eb91b2e53407d5c9018 Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Wed, 23 Oct 2024 10:44:00 +0000 Subject: [PATCH 1/3] feat: add env var support for logs and make config optional --- cmd/redactedhook/main.go | 74 ++++++++++++++++++++++++++++++++++++++-- docker-compose.yml | 17 ++++++--- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/cmd/redactedhook/main.go b/cmd/redactedhook/main.go index 17aa2d2..6d96dc0 100644 --- a/cmd/redactedhook/main.go +++ b/cmd/redactedhook/main.go @@ -98,6 +98,22 @@ func getEnv(key, defaultValue string) string { return defaultValue } +func hasRequiredEnvVars() bool { + // Check for essential environment variables + essentialVars := []string{ + "API_TOKEN", + "RED_APIKEY", + "OPS_APIKEY", + } + + for _, v := range essentialVars { + if _, exists := os.LookupEnv(envPrefix + v); !exists { + return false + } + } + return true +} + func initLogger() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "2006-01-02 15:04:05"}) } @@ -148,9 +164,40 @@ func startHTTPServer(ctx context.Context, address string) error { } func loadEnvironmentConfig() { + // Server settings + config.GetConfig().Server.Host = getEnv("HOST", config.GetConfig().Server.Host) + if port := os.Getenv(envPrefix + "PORT"); port != "" { + if val, err := fmt.Sscanf(port, "%d", &config.GetConfig().Server.Port); err != nil || val != 1 { + log.Warn().Msgf("Invalid PORT value: %s", port) + } + } + + // Authorization settings config.GetConfig().Authorization.APIToken = getEnv("API_TOKEN", config.GetConfig().Authorization.APIToken) config.GetConfig().IndexerKeys.REDKey = getEnv("RED_APIKEY", config.GetConfig().IndexerKeys.REDKey) config.GetConfig().IndexerKeys.OPSKey = getEnv("OPS_APIKEY", config.GetConfig().IndexerKeys.OPSKey) + + // Logs settings + config.GetConfig().Logs.LogLevel = getEnv("LOGS_LOGLEVEL", config.GetConfig().Logs.LogLevel) + config.GetConfig().Logs.LogToFile = os.Getenv(envPrefix+"LOGS_LOGTOFILE") == "true" + config.GetConfig().Logs.LogFilePath = getEnv("LOGS_LOGFILEPATH", config.GetConfig().Logs.LogFilePath) + + if maxSize := os.Getenv(envPrefix + "LOGS_MAXSIZE"); maxSize != "" { + if val, err := fmt.Sscanf(maxSize, "%d", &config.GetConfig().Logs.MaxSize); err != nil || val != 1 { + log.Warn().Msgf("Invalid LOGS_MAXSIZE value: %s", maxSize) + } + } + if maxBackups := os.Getenv(envPrefix + "LOGS_MAXBACKUPS"); maxBackups != "" { + if val, err := fmt.Sscanf(maxBackups, "%d", &config.GetConfig().Logs.MaxBackups); err != nil || val != 1 { + log.Warn().Msgf("Invalid LOGS_MAXBACKUPS value: %s", maxBackups) + } + } + if maxAge := os.Getenv(envPrefix + "LOGS_MAXAGE"); maxAge != "" { + if val, err := fmt.Sscanf(maxAge, "%d", &config.GetConfig().Logs.MaxAge); err != nil || val != 1 { + log.Warn().Msgf("Invalid LOGS_MAXAGE value: %s", maxAge) + } + } + config.GetConfig().Logs.Compress = os.Getenv(envPrefix+"LOGS_COMPRESS") == "true" } func healthHandler(w http.ResponseWriter, r *http.Request) { @@ -185,14 +232,35 @@ func main() { return } - config.InitConfig(configPath) + // Initialize with default values + config.GetConfig().Server.Host = "127.0.0.1" + config.GetConfig().Server.Port = 42135 + config.GetConfig().Logs.LogLevel = "info" + config.GetConfig().Logs.MaxSize = 100 // 100MB + config.GetConfig().Logs.MaxBackups = 3 + config.GetConfig().Logs.MaxAge = 28 // 28 days + config.GetConfig().Logs.LogFilePath = "redactedhook.log" + + // Try to load config file if it exists + configFileExists := false + if _, err := os.Stat(configPath); err == nil { + config.InitConfig(configPath) + configFileExists = true + } + + // If no config file and no environment variables, exit + if !configFileExists && !hasRequiredEnvVars() { + log.Fatal().Msg("No config file found and required environment variables are not set. Please provide either a config file or set the required environment variables (REDACTEDHOOK__API_TOKEN, REDACTEDHOOK__RED_APIKEY, REDACTEDHOOK__OPS_APIKEY)") + } + + // Load environment variables (these will override config file values if present) + loadEnvironmentConfig() + // Validate the final configuration if err := config.ValidateConfig(); err != nil { log.Fatal().Err(err).Msg("Invalid configuration") } - loadEnvironmentConfig() - http.HandleFunc(path, api.WebhookHandler) http.HandleFunc(healthPath, healthHandler) diff --git a/docker-compose.yml b/docker-compose.yml index fe0bd37..3a0efda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,11 +14,18 @@ services: cap_drop: - ALL environment: - #- REDACTEDHOOK__HOST=127.0.0.1 # Override the host from config.toml - #- REDACTEDHOOK__PORT=42135 # Override the port from config.toml - #- REDACTEDHOOK__API_TOKEN= # Override the API token from config.toml - #- REDACTEDHOOK__RED_APIKEY= # Override the red api_key from config.toml - #- REDACTEDHOOK__OPS_APIKEY= # Override the ops api_key from config.toml + #- REDACTEDHOOK__HOST=127.0.0.1 # string: Override the host from config.toml + #- REDACTEDHOOK__PORT=42135 # integer: Override the port from config.toml + #- REDACTEDHOOK__API_TOKEN= # string: Override the API token from config.toml + #- REDACTEDHOOK__RED_APIKEY= # string: Override the red api_key from config.toml + #- REDACTEDHOOK__OPS_APIKEY= # string: Override the ops api_key from config.toml + #- REDACTEDHOOK__LOGS_LOGLEVEL= # string: Override the log level from config.toml + #- REDACTEDHOOK__LOGS_LOGTOFILE= # boolean: Override log to file setting (true/false) + #- REDACTEDHOOK__LOGS_LOGFILEPATH= # string: Override the log file path from config.toml + #- REDACTEDHOOK__LOGS_MAXSIZE= # integer: Override max log file size in MB + #- REDACTEDHOOK__LOGS_MAXBACKUPS= # integer: Override max number of old log files to keep + #- REDACTEDHOOK__LOGS_MAXAGE= # integer: Override max age in days to keep log files + #- REDACTEDHOOK__LOGS_COMPRESS= # boolean: Override log compression setting (true/false) - TZ=UTC ports: - 127.0.0.1:42135:42135 From cb9ef35490d3f69fb11573f43746a3d7a0026d06 Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Wed, 23 Oct 2024 10:55:08 +0000 Subject: [PATCH 2/3] fix(ValidateConfig): remove log requirement --- internal/config/config_loader.go | 54 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/internal/config/config_loader.go b/internal/config/config_loader.go index 88d5b59..f18529a 100644 --- a/internal/config/config_loader.go +++ b/internal/config/config_loader.go @@ -188,33 +188,33 @@ func ValidateConfig() error { validationErrors = append(validationErrors, "Indexer OPSKey should not be empty") } - if !viper.IsSet("logs.loglevel") || viper.GetString("logs.loglevel") == "" { - validationErrors = append(validationErrors, "Log level is required") - } - - if !viper.IsSet("logs.logtofile") { - validationErrors = append(validationErrors, "Log to file flag is required") - } - - if viper.GetBool("logs.logtofile") && (!viper.IsSet("logs.logfilepath") || viper.GetString("logs.logfilepath") == "") { - validationErrors = append(validationErrors, "Log file path is required when logging to a file") - } - - if !viper.IsSet("logs.maxsize") || viper.GetInt("logs.maxsize") <= 0 { - validationErrors = append(validationErrors, "Max log file size should be a positive integer") - } - - if !viper.IsSet("logs.maxbackups") || viper.GetInt("logs.maxbackups") < 0 { - validationErrors = append(validationErrors, "Max backups should be a non-negative integer") - } - - if !viper.IsSet("logs.maxage") || viper.GetInt("logs.maxage") <= 0 { - validationErrors = append(validationErrors, "Max age should be a positive integer") - } - - if !viper.IsSet("logs.compress") { - validationErrors = append(validationErrors, "Compress flag is required") - } + //if !viper.IsSet("logs.loglevel") || viper.GetString("logs.loglevel") == "" { + // validationErrors = append(validationErrors, "Log level is required") + //} + // + //if !viper.IsSet("logs.logtofile") { + // validationErrors = append(validationErrors, "Log to file flag is required") + //} + // + //if viper.GetBool("logs.logtofile") && (!viper.IsSet("logs.logfilepath") || viper.GetString("logs.logfilepath") == "") { + // validationErrors = append(validationErrors, "Log file path is required when logging to a file") + //} + // + //if !viper.IsSet("logs.maxsize") || viper.GetInt("logs.maxsize") <= 0 { + // validationErrors = append(validationErrors, "Max log file size should be a positive integer") + //} + // + //if !viper.IsSet("logs.maxbackups") || viper.GetInt("logs.maxbackups") < 0 { + // validationErrors = append(validationErrors, "Max backups should be a non-negative integer") + //} + // + //if !viper.IsSet("logs.maxage") || viper.GetInt("logs.maxage") <= 0 { + // validationErrors = append(validationErrors, "Max age should be a positive integer") + //} + // + //if !viper.IsSet("logs.compress") { + // validationErrors = append(validationErrors, "Compress flag is required") + //} host := viper.GetString("server.host") if envHost, exists := os.LookupEnv("REDACTEDHOOK__HOST"); exists { From 77065dca584f41bd6819ee5bd900710f6c75d23e Mon Sep 17 00:00:00 2001 From: s0up4200 Date: Thu, 24 Oct 2024 20:22:14 +0000 Subject: [PATCH 3/3] feat: log healthz requests --- cmd/redactedhook/main.go | 8 +- cmd/redactedhook/main_test.go | 221 ++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 cmd/redactedhook/main_test.go diff --git a/cmd/redactedhook/main.go b/cmd/redactedhook/main.go index 6d96dc0..1bb0f65 100644 --- a/cmd/redactedhook/main.go +++ b/cmd/redactedhook/main.go @@ -27,7 +27,7 @@ var ( const ( path = "/hook" - healthPath = "/health" + healthPath = "/healthz" tokenLength = 16 shutdownTimeout = 10 * time.Second readTimeout = 10 * time.Second @@ -201,6 +201,12 @@ func loadEnvironmentConfig() { } func healthHandler(w http.ResponseWriter, r *http.Request) { + log.Info(). + Str("method", r.Method). + Str("remote_addr", r.RemoteAddr). + Str("user_agent", r.UserAgent()). + Msg("Health check request received") + w.WriteHeader(http.StatusOK) if _, err := w.Write([]byte("OK")); err != nil { log.Error().Err(err).Msg("Failed to write health check response") diff --git a/cmd/redactedhook/main_test.go b/cmd/redactedhook/main_test.go new file mode 100644 index 0000000..11d1a20 --- /dev/null +++ b/cmd/redactedhook/main_test.go @@ -0,0 +1,221 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/s0up4200/redactedhook/internal/config" +) + +func TestGenerateAPIToken(t *testing.T) { + token, err := generateAPIToken() + if err != nil { + t.Errorf("generateAPIToken() error = %v", err) + } + if len(token) != tokenLength*2 { // *2 because hex encoding doubles length + t.Errorf("generateAPIToken() token length = %v, want %v", len(token), tokenLength*2) + } +} + +func TestGetEnv(t *testing.T) { + tests := []struct { + name string + key string + defaultVal string + envVal string + expected string + shouldSetEnv bool + }{ + { + name: "returns default when env not set", + key: "TEST_KEY", + defaultVal: "default", + envVal: "", + expected: "default", + shouldSetEnv: false, + }, + { + name: "returns env value when set", + key: "TEST_KEY", + defaultVal: "default", + envVal: "custom", + expected: "custom", + shouldSetEnv: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldSetEnv { + os.Setenv(envPrefix+tt.key, tt.envVal) + defer os.Unsetenv(envPrefix + tt.key) + } + + if got := getEnv(tt.key, tt.defaultVal); got != tt.expected { + t.Errorf("getEnv() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestHasRequiredEnvVars(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + expected bool + }{ + { + envVars: map[string]string{ + envPrefix + "API_TOKEN": "token", + envPrefix + "RED_APIKEY": "red", + envPrefix + "OPS_APIKEY": "ops", + }, + expected: true, + }, + { + name: "missing one required var", + envVars: map[string]string{ + envPrefix + "API_TOKEN": "token", + envPrefix + "RED_APIKEY": "red", + }, + expected: false, + }, + { + name: "no vars present", + envVars: map[string]string{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear environment before each test + os.Clearenv() + + // Set test environment variables + for k, v := range tt.envVars { + os.Setenv(k, v) + } + + if got := hasRequiredEnvVars(); got != tt.expected { + t.Errorf("hasRequiredEnvVars() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestLoadEnvironmentConfig(t *testing.T) { + // Save original config + originalConfig := config.GetConfig() + defer func() { + // Restore original config after test + config.GetConfig().Server = originalConfig.Server + config.GetConfig().Authorization = originalConfig.Authorization + config.GetConfig().IndexerKeys = originalConfig.IndexerKeys + config.GetConfig().Logs = originalConfig.Logs + }() + + tests := []struct { + name string + env map[string]string + check func(t *testing.T) + }{ + { + name: "server settings", + env: map[string]string{ + envPrefix + "HOST": "0.0.0.0", + envPrefix + "PORT": "8080", + }, + check: func(t *testing.T) { + if config.GetConfig().Server.Host != "0.0.0.0" { + t.Errorf("Host = %v, want %v", config.GetConfig().Server.Host, "0.0.0.0") + } + if config.GetConfig().Server.Port != 8080 { + t.Errorf("Port = %v, want %v", config.GetConfig().Server.Port, 8080) + } + }, + }, + { + name: "authorization settings", + env: map[string]string{ + envPrefix + "API_TOKEN": "test-token", + envPrefix + "RED_APIKEY": "red-key", + envPrefix + "OPS_APIKEY": "ops-key", + }, + check: func(t *testing.T) { + if config.GetConfig().Authorization.APIToken != "test-token" { + t.Errorf("APIToken = %v, want %v", config.GetConfig().Authorization.APIToken, "test-token") + } + if config.GetConfig().IndexerKeys.REDKey != "red-key" { + t.Errorf("REDKey = %v, want %v", config.GetConfig().IndexerKeys.REDKey, "red-key") + } + if config.GetConfig().IndexerKeys.OPSKey != "ops-key" { + t.Errorf("OPSKey = %v, want %v", config.GetConfig().IndexerKeys.OPSKey, "ops-key") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear environment before each test + os.Clearenv() + + // Set test environment variables + for k, v := range tt.env { + os.Setenv(k, v) + } + + loadEnvironmentConfig() + tt.check(t) + }) + } +} + +func TestCreateServer(t *testing.T) { + address := "localhost:8080" + server := createServer(address) + + if server.Addr != address { + t.Errorf("server.Addr = %v, want %v", server.Addr, address) + } + + if server.ReadTimeout != readTimeout { + t.Errorf("server.ReadTimeout = %v, want %v", server.ReadTimeout, readTimeout) + } + + if server.WriteTimeout != writeTimeout { + t.Errorf("server.WriteTimeout = %v, want %v", server.WriteTimeout, writeTimeout) + } + + if server.IdleTimeout != idleTimeout { + t.Errorf("server.IdleTimeout = %v, want %v", server.IdleTimeout, idleTimeout) + } + + if server.ReadHeaderTimeout != readHeaderTimeout { + t.Errorf("server.ReadHeaderTimeout = %v, want %v", server.ReadHeaderTimeout, readHeaderTimeout) + } +} + +func TestHealthHandler(t *testing.T) { + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(healthHandler) + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + expected := "OK" + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) + } +}