use COnfig struct to parse env variables

This commit is contained in:
Ruidy 2025-03-23 22:57:22 +01:00
parent 26207baee8
commit 75eb3b8502
No known key found for this signature in database
GPG key ID: E00F51288CB857CC
6 changed files with 86 additions and 140 deletions

1
go.mod
View file

@ -45,6 +45,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // 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/stretchr/objx v0.5.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect

2
go.sum
View file

@ -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/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 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View file

@ -1,29 +1,65 @@
package config package config
import ( import (
"os" "context"
"fmt"
"time"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/labstack/gommon/log" "github.com/sethvargo/go-envconfig"
) )
type Config map[string]string // Config holds the application configuration settings loaded from environment variables
type Config struct {
var DefaultConfig = Config{ // AppName is the name of the application
"PORT": "8000", AppName string `env:"APP_NAME, default=rentease"`
"DEBUG": "false", // 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() _ = godotenv.Load()
log.Info("loaded env variables") config := new(Config)
if err := envconfig.ProcessWith(ctx, &envconfig.Config{
return func(key string) string { Target: config,
if value := os.Getenv(key); value != "" { Lookuper: envconfig.PrefixLookuper("APP_", envconfig.OsLookuper()),
return value }); err != nil {
} else { return nil, fmt.Errorf("could not parse environment variables: %w", err)
log.Warnf("no value found for %s using defaults", key)
return DefaultConfig[key]
}
} }
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
} }

View file

@ -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)
}
}
}
}

View file

@ -43,18 +43,16 @@ type parserClient interface {
} }
type Service struct { type Service struct {
store Store store Store
parser parserClient parser parserClient
calendar CalendarClient pdf PdfClient
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{ return &Service{
store: store, store: store,
parser: parser, parser: parser,
calendar: calendar, pdf: pdf,
pdf: pdf,
}, nil }, nil
} }

49
main.go
View file

@ -5,14 +5,11 @@ import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"strconv"
"strings"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/rjNemo/rentease/assets" "github.com/rjNemo/rentease/assets"
"github.com/rjNemo/rentease/internal/config" "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/database"
"github.com/rjNemo/rentease/internal/driver/parser" "github.com/rjNemo/rentease/internal/driver/parser"
"github.com/rjNemo/rentease/internal/driver/pdf" "github.com/rjNemo/rentease/internal/driver/pdf"
@ -25,19 +22,24 @@ import (
func main() { func main() {
ctx := context.Background() ctx := context.Background()
if err := run(ctx, config.NewConfig()); err != nil { if err := run(ctx); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err) fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1) 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) ctx, cancel := signal.NotifyContext(c, os.Interrupt)
defer cancel() defer cancel()
appConfig, err := config.New(ctx)
if err != nil {
return err
}
// init sentry // init sentry
if err := sentry.Init(sentry.ClientOptions{ if err := sentry.Init(sentry.ClientOptions{
Dsn: getEnv("SENTRY_DSN"), Dsn: appConfig.SentryDsn,
EnableTracing: true, EnableTracing: true,
TracesSampleRate: 1.0, TracesSampleRate: 1.0,
}); err != nil { }); err != nil {
@ -45,7 +47,7 @@ func run(c context.Context, getEnv func(string) string) error {
} }
// init database // init database
db, err := database.New(getEnv("DATABASE_URL")) db, err := database.New(appConfig.DatabaseUrl)
if err != nil { if err != nil {
return fmt.Errorf("error connecting to the database %w", err) 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) bookingStore := bookingRepo.NewPgStore(db)
gc, err := calendar.NewGoogleClient(ctx, getEnv("CALENDAR_CREDENTIALS")) // gc, err := calendar.NewGoogleClient(ctx, appConfig.CalendarCredentials)
if err != nil { // if err != nil {
return fmt.Errorf("error building calendar client %w", err) // return fmt.Errorf("error building calendar client %w", err)
} // }
// build pdf client // build pdf client
pc, err := pdf.NewPdfClient() 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) 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 { if err != nil {
return fmt.Errorf("error creating booking service: %w", err) return fmt.Errorf("error creating booking service: %w", err)
} }
// build authentication service // build authentication service
as, err := auth.NewService( as, err := auth.NewService(
getEnv("SESSION_SECRET"), appConfig.SessionSecret,
getEnv("ADMIN"), appConfig.Admin,
getEnv("ADMIN_SECRET"), appConfig.AdminSecret,
getEnv("API_KEY"), appConfig.ApiKey,
) )
if err != nil { if err != nil {
return fmt.Errorf("error starting auth service %w", err) return fmt.Errorf("error starting auth service %w", err)
} }
p := getEnv("PORT") port := appConfig.Port
port, err := strconv.Atoi(p)
if err != nil {
return fmt.Errorf("error parsing PORT env %w", err)
}
ogs := getEnv("ORIGINS") origins := appConfig.Origins
origins := strings.Split(ogs, ",")
srv, err := server.New( srv, err := server.New(
bookingService, 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 config.NewHost(), // TODO: move to the database at some point
server.WithPort(port), server.WithPort(port),
server.WithFileSystem(assets.Static), server.WithFileSystem(assets.Static),
server.WithDebug(strings.ToLower(getEnv("DEBUG")) == "true"), server.WithDebug(appConfig.Debug),
server.WithSecretKey(getEnv("SECRET_KEY")), server.WithSecretKey(appConfig.SecretKey),
server.WithOrigins(origins), server.WithOrigins(origins),
) )
if err != nil { if err != nil {