From 5543a19df1e3aa6563b3112d504dd00e3c366270 Mon Sep 17 00:00:00 2001 From: Felipe Martin Garcia <812088+fmartingr@users.noreply.github.com> Date: Tue, 9 Aug 2022 18:27:37 +0200 Subject: [PATCH] feat: added server configuration using env vars (#7) * feat: added server configuration using env vars * updated readme * refactor: removed error returns where unneeded --- README.md | 53 +++++++++++++++++++++++--------- cmd/bazaar/main.go | 13 +++++--- go.mod | 1 + go.sum | 3 ++ internal/server/config.go | 64 +++++++++++++++++++++++++++++++++++++++ internal/server/http.go | 4 ++- internal/server/server.go | 54 ++++++++++++++++++--------------- pkg/manager/main.go | 8 ++--- 8 files changed, 150 insertions(+), 50 deletions(-) create mode 100644 internal/server/config.go diff --git a/README.md b/README.md index 4da272f..ba1e3d6 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,60 @@ A service/library to extract product information from URLs. +## Configuration + +| Variable name | Default | Description | +| -------------- | ------- | ------------------------------ | +| `HTTP_ENABLED` | `true` | Enable/Disable the HTTP server | +| `HTTP_PORT` | `8080` | Port to serve the HTTP in | + +## Servers + +### HTTP + +- `POST /item` + + Parameters: + - **url**: The URL to extract information from + + Responses: + - `200`: Information extracted + - `400`: Shop not supported, missing parameters + - `500`: Internal error, check logs + + +- `GET /liveness` + + Responses: + - `200`: Server running + + ## Data model Currently, this information is extracted from the site (if possbile): ``` js { - "image_url": "", // (string) URL to an image file - "in_stock": false, // (bool) If the item is currently available for purchase - "name": "", // (string) The name of the product as it appears on the site - "price": 14.21, // (optional, float) The price of the product [parsed by the library] - "price_text": "14,21 €", // (optional, string) The price of the product as it appears on the site (with currency) - "release_date": "2021-03-22T00:00:00Z", // (optional, string RFC3339) the release date of the item - "url": "" // (string) The URL of the item + "description": "...", + "image_url": "https://...", + "in_stock": false, + "name": "...", + "price": 0.0, + "price_text": "0,0 €", + "release_date": "2019-03-08T00:00:00Z", + "url": "https://..." } ``` +[pkg/models/product.go](./pkg/models/product.go) + ## Supported sites Support is handled in a _best effort_ basis. Some sites do not provided all exposed fields. - [Amazon.es](https://amazon.es) - [Amazon.com](https://amazon.com) -- [Akira Comics](https://www.akiracomics.com) +- [AkiraComics](https://www.akiracomics.com) - [Casa del libro](https://www.casadellibro.com) - [Heroes De Papel](https://heroesdepapel.es) - [Steam](https://store.steampowered.com) - -## Running - -``` -go run cmd/server/main.go -``` diff --git a/cmd/bazaar/main.go b/cmd/bazaar/main.go index bed1528..51508f8 100644 --- a/cmd/bazaar/main.go +++ b/cmd/bazaar/main.go @@ -1,6 +1,9 @@ package main import ( + "context" + "log" + "github.com/fmartingr/bazaar/internal/server" "github.com/fmartingr/bazaar/pkg/manager" "github.com/fmartingr/bazaar/pkg/shop/akiracomics" @@ -20,11 +23,13 @@ func main() { m.Register(gtmstore.Domains, gtmstore.NewGTMStoreShopFactory()) m.Register(casadellibro.Domains, casadellibro.NewCasaDelLibroShopFactory()) - server := server.NewServer(server.ServerConf{ - HttpPort: 8080, - }, &m) + ctx := context.Background() - server.Start() + server := server.NewServer(server.ParseServerConfiguration(ctx), &m) + + if err := server.Start(ctx); err != nil { + log.Panicf("error starting server: %s", err) + } server.WaitStop() } diff --git a/go.mod b/go.mod index 1057ca1..d8b98e3 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/PuerkitoBio/goquery v1.8.0 github.com/goodsign/monday v1.0.0 + github.com/sethvargo/go-envconfig v0.8.2 github.com/stretchr/testify v1.8.0 ) diff --git a/go.sum b/go.sum index ebb0aa3..3c5dd01 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc= github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-envconfig v0.8.2 h1:DDUVuG21RMgeB/bn4leclUI/837y6cQCD4w8hb5797k= +github.com/sethvargo/go-envconfig v0.8.2/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= 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/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/server/config.go b/internal/server/config.go new file mode 100644 index 0000000..baeaa3e --- /dev/null +++ b/internal/server/config.go @@ -0,0 +1,64 @@ +package server + +import ( + "bufio" + "context" + "log" + "os" + "strings" + + "github.com/sethvargo/go-envconfig" +) + +// readDotEnv reads the configuration from variables in a .env file (only for contributing) +func readDotEnv() map[string]string { + file, err := os.Open(".env") + if err != nil { + return nil + } + defer file.Close() + + result := make(map[string]string) + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "#") { + continue + } + + keyval := strings.SplitN(line, "=", 2) + result[keyval[0]] = keyval[1] + } + + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + + return result +} + +type ServerConfig struct { + Hostname string `env:"HOSTNAME,required"` + Http struct { + Enabled bool `env:"HTTP_ENABLED,default=True"` + Port int `env:"HTTP_PORT,default=8080"` + } +} + +func ParseServerConfiguration(ctx context.Context) *ServerConfig { + var cfg ServerConfig + + lookuper := envconfig.MultiLookuper( + envconfig.MapLookuper(map[string]string{"HOSTNAME": os.Getenv("HOSTNAME")}), + envconfig.MapLookuper(readDotEnv()), + envconfig.PrefixLookuper("BAZAAR_", envconfig.OsLookuper()), + envconfig.OsLookuper(), + ) + if err := envconfig.ProcessWith(ctx, &cfg, lookuper); err != nil { + log.Fatalf("Error parsing configuration: %s", err) + } + + return &cfg +} diff --git a/internal/server/http.go b/internal/server/http.go index 8bc849a..68d139f 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -50,7 +50,9 @@ func NewHttpServer(port int, m *manager.Manager) *httpServer { payload, _ := json.Marshal(product) rw.Header().Add("Content-Type", "application/json") - rw.Write(payload) + if _, err := rw.Write(payload); err != nil { + log.Printf("error writting response: %s", err) + } }) mux.HandleFunc("/liveness", func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/server/server.go b/internal/server/server.go index 7b5fccc..b0e7ff7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -14,54 +14,58 @@ import ( "github.com/fmartingr/bazaar/pkg/manager" ) -type ServerConf struct { - HttpPort int -} - type Server struct { - http internalModels.Server + Http internalModels.Server + config *ServerConfig cancel context.CancelFunc } -func (s *Server) Start() error { - ctx, cancel := context.WithCancel(context.Background()) +func (s *Server) Start(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) s.cancel = cancel - go func() { - if err := s.http.Start(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("error starting server: %s", err) - } - }() + if s.config.Http.Enabled { + go func() { + if err := s.Http.Start(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("error starting server: %s", err) + } + }() + } return nil } -func (s *Server) WaitStop() error { +func (s *Server) WaitStop() { signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) sig := <-signals log.Printf("signal %s received, shutting down", sig) - return s.Stop() + s.Stop() } -func (s *Server) Stop() error { +func (s *Server) Stop() { s.cancel() shuwdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if err := s.http.Stop(shuwdownContext); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("error shutting down http server: %s", err) - } - - return nil -} - -func NewServer(serverConf ServerConf, m *manager.Manager) *Server { - return &Server{ - http: NewHttpServer(serverConf.HttpPort, m), + if s.config.Http.Enabled { + if err := s.Http.Stop(shuwdownContext); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("error shutting down http server: %s", err) + } } } + +func NewServer(conf *ServerConfig, m *manager.Manager) *Server { + server := &Server{ + config: conf, + } + if conf.Http.Enabled { + server.Http = NewHttpServer(conf.Http.Port, m) + } + + return server +} diff --git a/pkg/manager/main.go b/pkg/manager/main.go index dcca135..32e9078 100644 --- a/pkg/manager/main.go +++ b/pkg/manager/main.go @@ -14,19 +14,15 @@ type Manager struct { domains map[string]models.Shop } -func (m *Manager) Register(domains []string, shopFactory models.ShopFactory) error { +func (m *Manager) Register(domains []string, shopFactory models.ShopFactory) { baseShop := models.NewShopOptions(clients.NewBasicHttpClient()) shop := shopFactory(baseShop) for _, domain := range domains { - if _, exists := m.domains[domain]; exists { - return fmt.Errorf("domain %s is already registered", domain) - } else { + if _, exists := m.domains[domain]; !exists { m.domains[domain] = shop } } - - return nil } func (m *Manager) GetShop(host string) models.Shop {