Skip to content
This repository has been archived by the owner on Dec 23, 2024. It is now read-only.

feat: add env var support for logs and make config optional #36

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 78 additions & 4 deletions cmd/redactedhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var (

const (
path = "/hook"
healthPath = "/health"
healthPath = "/healthz"
tokenLength = 16
shutdownTimeout = 10 * time.Second
readTimeout = 10 * time.Second
Expand Down Expand Up @@ -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"})
}
Expand Down Expand Up @@ -148,12 +164,49 @@ 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) {
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")
Expand Down Expand Up @@ -185,14 +238,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)

Expand Down
221 changes: 221 additions & 0 deletions cmd/redactedhook/main_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
17 changes: 12 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading