From 75eb3b850297e62653998a0c0598d79934d607cf Mon Sep 17 00:00:00 2001 From: Ruidy Date: Sun, 23 Mar 2025 22:57:22 +0100 Subject: [PATCH] use COnfig struct to parse env variables --- go.mod | 1 + go.sum | 2 + internal/config/config.go | 70 +++++++++++++++++------ internal/cron/cron.go | 88 ----------------------------- internal/service/booking/service.go | 16 +++--- main.go | 49 ++++++++-------- 6 files changed, 86 insertions(+), 140 deletions(-) delete mode 100644 internal/cron/cron.go diff --git a/go.mod b/go.mod index 408a635..e665c85 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sethvargo/go-envconfig v1.1.1 github.com/stretchr/objx v0.5.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect diff --git a/go.sum b/go.sum index 2fc0d4d..77a55e4 100644 --- a/go.sum +++ b/go.sum @@ -142,6 +142,8 @@ github.com/rjNemo/underscore v0.7.0 h1:CeWQaDDWl541/gCj7ti8W7/koXNHwu73Riuc4SQZj github.com/rjNemo/underscore v0.7.0/go.mod h1:NJl2GYBIOdEaXdTD/MDyKgG6Wq7ZT+BOXlrU8GZEbdc= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sethvargo/go-envconfig v1.1.1 h1:JDu8Q9baIzJf47NPkzhIB6aLYL0vQ+pPypoYrejS9QY= +github.com/sethvargo/go-envconfig v1.1.1/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/internal/config/config.go b/internal/config/config.go index ec38637..56eabc3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,29 +1,65 @@ package config import ( - "os" + "context" + "fmt" + "time" "github.com/joho/godotenv" - "github.com/labstack/gommon/log" + "github.com/sethvargo/go-envconfig" ) -type Config map[string]string - -var DefaultConfig = Config{ - "PORT": "8000", - "DEBUG": "false", +// Config holds the application configuration settings loaded from environment variables +type Config struct { + // AppName is the name of the application + AppName string `env:"APP_NAME, default=rentease"` + // DatabaseUrl is the connection string for the database + DatabaseUrl string `env:"DATABASE_URL, required"` + // SecretKey is the secret key used for JWT token signing + SecretKey string `env:"SECRET_KEY, required"` + // SessionSecret is the secret key used for session signing + SessionSecret string `env:"SESSION_SECRET, required"` + // ApiKey is the API access key + ApiKey string `env:"API_KEY, required"` + // Admin is the email used to access the admin panel + Admin string `env:"ADMIN, required"` + // AdminSecret is the password used to access the admin panel + AdminSecret string `env:"ADMIN_SECRET, required"` + // Origins is the list of allowed origins + Origins []string `env:"ORIGINS, required"` + // Port is the HTTP server port number + Port int `env:"PORT, default=4200"` + // Debug enables debug mode when true + Debug bool `env:"DEBUG, default=false"` + // ParserBaseUrl is the base url for the parser service + ParserBaseUrl string `env:"PARSER_BASE_URL, required"` + // RequestTimeout is the maximum time allowed for a request. It prevents slowloris attacks that result in DDOS + RequestTimeout time.Duration `env:"REQUEST_TIMEOUT, default=5s"` + // SentryDsn is the DSN for Sentry error reporting + SentryDsn string `env:"SENTRY_DSN"` } -func NewConfig() func(string) string { +// New creates a [Config] struct. It first parses the environment variables. You can use a .env file. +// Please note that the env variables must be prefix with "REEMIND_" to be accounted for. +func New(ctx context.Context) (*Config, error) { _ = godotenv.Load() - log.Info("loaded env variables") - - return func(key string) string { - if value := os.Getenv(key); value != "" { - return value - } else { - log.Warnf("no value found for %s using defaults", key) - return DefaultConfig[key] - } + config := new(Config) + if err := envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: config, + Lookuper: envconfig.PrefixLookuper("APP_", envconfig.OsLookuper()), + }); err != nil { + return nil, fmt.Errorf("could not parse environment variables: %w", err) } + + return config, nil +} + +// MustConfig is a helper that wraps [New] and panics if an error occurs. +// Use it in cases a [Config] is required and you want to exit the application if an error occurs. +func MustConfig(ctx context.Context) *Config { + c, err := New(ctx) + if err != nil { + panic(err) + } + return c } diff --git a/internal/cron/cron.go b/internal/cron/cron.go deleted file mode 100644 index 637276f..0000000 --- a/internal/cron/cron.go +++ /dev/null @@ -1,88 +0,0 @@ -package cron - -import ( - "fmt" - "log" - "time" -) - -// Cron handles jobs scheduling and execution -type Cron struct { - jobs []Job - ErrChan chan error - DoneChan chan struct{} - SuccessChan chan string -} - -// Job is a type that holds the details for each job. -type Job struct { - Name string - Schedule string - Action JobFunc -} - -type JobFunc func() error - -func New() *Cron { - return &Cron{ - jobs: make([]Job, 0), - ErrChan: make(chan error), - SuccessChan: make(chan string), - DoneChan: make(chan struct{}), - } -} - -func (c *Cron) AddJob(job Job) { - c.jobs = append(c.jobs, job) -} - -func (c *Cron) Start() { - for _, j := range c.jobs { - go c.scheduleJob(j) - } -} - -func (c *Cron) Stop() { - close(c.DoneChan) - close(c.SuccessChan) - close(c.ErrChan) -} - -// scheduleJob adds a task to the Cron schedule based on its schedule -func (c *Cron) scheduleJob(j Job) { - for { - select { - case <-c.DoneChan: - log.Printf("stopping job %s", j.Name) - return - default: - now := time.Now() - - var next time.Time - switch j.Schedule { - case "minute": - next = now.Add(10 * time.Second) - case "daily": - next = now.AddDate(0, 0, 1).Truncate(24 * time.Hour) - case "weekly": - next = now.AddDate(0, 0, 7).Truncate(24 * time.Hour) - case "monthly": - nextMonth := now.AddDate(0, 1, 0) - next = time.Date(nextMonth.Year(), nextMonth.Month(), 1, 0, 0, 0, 0, nextMonth.Location()) - default: - log.Printf("Unknown schedule %q for job %q", j.Schedule, j.Name) - return - } - - sleepDuration := time.Until(next) - log.Printf("Job %q will run in %s", j.Name, sleepDuration.String()) - time.Sleep(sleepDuration) - - if err := j.Action(); err != nil { - c.ErrChan <- fmt.Errorf("job %s failed: %w", j.Name, err) - } else { - c.SuccessChan <- fmt.Sprintf("job %s completed successfully", j.Name) - } - } - } -} diff --git a/internal/service/booking/service.go b/internal/service/booking/service.go index a2fe21f..e5031eb 100644 --- a/internal/service/booking/service.go +++ b/internal/service/booking/service.go @@ -43,18 +43,16 @@ type parserClient interface { } type Service struct { - store Store - parser parserClient - calendar CalendarClient - pdf PdfClient + store Store + parser parserClient + pdf PdfClient } -func NewService(store Store, parser parserClient, calendar CalendarClient, pdf PdfClient) (*Service, error) { +func NewService(store Store, parser parserClient, pdf PdfClient) (*Service, error) { return &Service{ - store: store, - parser: parser, - calendar: calendar, - pdf: pdf, + store: store, + parser: parser, + pdf: pdf, }, nil } diff --git a/main.go b/main.go index a982f43..9f088c7 100644 --- a/main.go +++ b/main.go @@ -5,14 +5,11 @@ import ( "fmt" "os" "os/signal" - "strconv" - "strings" "github.com/getsentry/sentry-go" "github.com/rjNemo/rentease/assets" "github.com/rjNemo/rentease/internal/config" - "github.com/rjNemo/rentease/internal/driver/calendar" "github.com/rjNemo/rentease/internal/driver/database" "github.com/rjNemo/rentease/internal/driver/parser" "github.com/rjNemo/rentease/internal/driver/pdf" @@ -25,19 +22,24 @@ import ( func main() { ctx := context.Background() - if err := run(ctx, config.NewConfig()); err != nil { + if err := run(ctx); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } } -func run(c context.Context, getEnv func(string) string) error { +func run(c context.Context) error { ctx, cancel := signal.NotifyContext(c, os.Interrupt) defer cancel() + appConfig, err := config.New(ctx) + if err != nil { + return err + } + // init sentry if err := sentry.Init(sentry.ClientOptions{ - Dsn: getEnv("SENTRY_DSN"), + Dsn: appConfig.SentryDsn, EnableTracing: true, TracesSampleRate: 1.0, }); err != nil { @@ -45,7 +47,7 @@ func run(c context.Context, getEnv func(string) string) error { } // init database - db, err := database.New(getEnv("DATABASE_URL")) + db, err := database.New(appConfig.DatabaseUrl) if err != nil { return fmt.Errorf("error connecting to the database %w", err) } @@ -56,10 +58,10 @@ func run(c context.Context, getEnv func(string) string) error { bookingStore := bookingRepo.NewPgStore(db) - gc, err := calendar.NewGoogleClient(ctx, getEnv("CALENDAR_CREDENTIALS")) - if err != nil { - return fmt.Errorf("error building calendar client %w", err) - } + // gc, err := calendar.NewGoogleClient(ctx, appConfig.CalendarCredentials) + // if err != nil { + // return fmt.Errorf("error building calendar client %w", err) + // } // build pdf client pc, err := pdf.NewPdfClient() @@ -67,32 +69,27 @@ func run(c context.Context, getEnv func(string) string) error { return fmt.Errorf("error starting pdf client %w", err) } - parsingClient := parser.NewBookingAgentParser(getEnv("PARSER_BASE_URL")) + parsingClient := parser.NewBookingAgentParser(appConfig.ParserBaseUrl) - bookingService, err := booking.NewService(bookingStore, parsingClient, gc, pc) + bookingService, err := booking.NewService(bookingStore, parsingClient, pc) if err != nil { return fmt.Errorf("error creating booking service: %w", err) } // build authentication service as, err := auth.NewService( - getEnv("SESSION_SECRET"), - getEnv("ADMIN"), - getEnv("ADMIN_SECRET"), - getEnv("API_KEY"), + appConfig.SessionSecret, + appConfig.Admin, + appConfig.AdminSecret, + appConfig.ApiKey, ) if err != nil { return fmt.Errorf("error starting auth service %w", err) } - p := getEnv("PORT") - port, err := strconv.Atoi(p) - if err != nil { - return fmt.Errorf("error parsing PORT env %w", err) - } + port := appConfig.Port - ogs := getEnv("ORIGINS") - origins := strings.Split(ogs, ",") + origins := appConfig.Origins srv, err := server.New( bookingService, @@ -100,8 +97,8 @@ func run(c context.Context, getEnv func(string) string) error { config.NewHost(), // TODO: move to the database at some point server.WithPort(port), server.WithFileSystem(assets.Static), - server.WithDebug(strings.ToLower(getEnv("DEBUG")) == "true"), - server.WithSecretKey(getEnv("SECRET_KEY")), + server.WithDebug(appConfig.Debug), + server.WithSecretKey(appConfig.SecretKey), server.WithOrigins(origins), ) if err != nil {