initial commit

This commit is contained in:
Marco Realacci 2024-10-01 23:55:57 +02:00
commit 213b1aad6c
714 changed files with 590265 additions and 0 deletions

View file

@ -0,0 +1,48 @@
/*
Healthcheck is a simple program that sends an HTTP request to the local host (self) to a configured port number.
It's used in environment where you need a simple probe for health checks (e.g., an empty container in docker).
The probe URL is http://localhost:3000/liveness . Only the port can be changed.
Usage:
healthcheck [flags]
The flags are:
-port <1-65535>
Change the port where the request is sent.
Return values (exit codes):
0
The request was successful (HTTP 200 or HTTP 204)
> 0
The request was not successful (connection error or unexpected HTTP status code)
*/
package main
import (
"flag"
"fmt"
"net/http"
"os"
)
func main() {
var port = flag.Int("port", 3000, "HTTP port for healthcheck")
flag.Parse()
res, err := http.Get(fmt.Sprintf("http://localhost:%d/liveness", *port))
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
} else if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
_ = res.Body.Close()
_, _ = fmt.Fprintln(os.Stderr, "Healthcheck request not OK: ", res.Status)
os.Exit(1)
}
_ = res.Body.Close()
os.Exit(0)
}

21
cmd/webapi/cors.go Normal file
View file

@ -0,0 +1,21 @@
package main
import (
"github.com/gorilla/handlers"
"net/http"
)
// applyCORSHandler applies a CORS policy to the router. CORS stands for Cross-Origin Resource Sharing: it's a security
// feature present in web browsers that blocks JavaScript requests going across different domains if not specified in a
// policy. This function sends the policy of this API server.
func applyCORSHandler(h http.Handler) http.Handler {
return handlers.CORS(
handlers.AllowedHeaders([]string{
"x-example-header",
}),
handlers.AllowedMethods([]string{"GET", "POST", "OPTIONS", "DELETE", "PUT"}),
// Do not modify the CORS origin and max age, they are used in the evaluation.
handlers.AllowedOrigins([]string{"*"}),
handlers.MaxAge(1),
)(h)
}

View file

@ -0,0 +1,70 @@
package main
import (
"errors"
"fmt"
"github.com/ardanlabs/conf"
"gopkg.in/yaml.v2"
"io"
"os"
"time"
)
// WebAPIConfiguration describes the web API configuration. This structure is automatically parsed by
// loadConfiguration and values from flags, environment variable or configuration file will be loaded.
type WebAPIConfiguration struct {
Config struct {
Path string `conf:"default:/conf/config.yml"`
}
Web struct {
APIHost string `conf:"default:0.0.0.0:3000"`
DebugHost string `conf:"default:0.0.0.0:4000"`
ReadTimeout time.Duration `conf:"default:5s"`
WriteTimeout time.Duration `conf:"default:5s"`
ShutdownTimeout time.Duration `conf:"default:5s"`
}
Debug bool
DB struct {
Filename string `conf:"default:/tmp/decaf.db"`
}
}
// loadConfiguration creates a WebAPIConfiguration starting from flags, environment variables and configuration file.
// It works by loading environment variables first, then update the config using command line flags, finally loading the
// configuration file (specified in WebAPIConfiguration.Config.Path).
// So, CLI parameters will override the environment, and configuration file will override everything.
// Note that the configuration file can be specified only via CLI or environment variable.
func loadConfiguration() (WebAPIConfiguration, error) {
var cfg WebAPIConfiguration
// Try to load configuration from environment variables and command line switches
if err := conf.Parse(os.Args[1:], "CFG", &cfg); err != nil {
if errors.Is(err, conf.ErrHelpWanted) {
usage, err := conf.Usage("CFG", &cfg)
if err != nil {
return cfg, fmt.Errorf("generating config usage: %w", err)
}
fmt.Println(usage) //nolint:forbidigo
return cfg, conf.ErrHelpWanted
}
return cfg, fmt.Errorf("parsing config: %w", err)
}
// Override values from YAML if specified and if it exists (useful in k8s/compose)
fp, err := os.Open(cfg.Config.Path)
if err != nil && !os.IsNotExist(err) {
return cfg, fmt.Errorf("can't read the config file, while it exists: %w", err)
} else if err == nil {
yamlFile, err := io.ReadAll(fp)
if err != nil {
return cfg, fmt.Errorf("can't read config file: %w", err)
}
err = yaml.Unmarshal(yamlFile, &cfg)
if err != nil {
return cfg, fmt.Errorf("can't unmarshal config file: %w", err)
}
_ = fp.Close()
}
return cfg, nil
}

184
cmd/webapi/main.go Normal file
View file

@ -0,0 +1,184 @@
/*
Webapi is the executable for the main web server.
It builds a web server around APIs from `service/api`.
Webapi connects to external resources needed (database) and starts two web servers: the API web server, and the debug.
Everything is served via the API web server, except debug variables (/debug/vars) and profiler infos (pprof).
Usage:
webapi [flags]
Flags and configurations are handled automatically by the code in `load-configuration.go`.
Return values (exit codes):
0
The program ended successfully (no errors, stopped by signal)
> 0
The program ended due to an error
Note that this program will update the schema of the database to the latest version available (embedded in the
executable during the build).
*/
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"git.sapienzaapps.it/fantasticcoffee/fantastic-coffee-decaffeinated/service/api"
"git.sapienzaapps.it/fantasticcoffee/fantastic-coffee-decaffeinated/service/database"
"git.sapienzaapps.it/fantasticcoffee/fantastic-coffee-decaffeinated/service/globaltime"
"github.com/ardanlabs/conf"
_ "github.com/mattn/go-sqlite3"
"github.com/sirupsen/logrus"
"math/rand"
"net/http"
"os"
"os/signal"
"syscall"
)
// main is the program entry point. The only purpose of this function is to call run() and set the exit code if there is
// any error
func main() {
if err := run(); err != nil {
_, _ = fmt.Fprintln(os.Stderr, "error: ", err)
os.Exit(1)
}
}
// run executes the program. The body of this function should perform the following steps:
// * reads the configuration
// * creates and configure the logger
// * connects to any external resources (like databases, authenticators, etc.)
// * creates an instance of the service/api package
// * starts the principal web server (using the service/api.Router.Handler() for HTTP handlers)
// * waits for any termination event: SIGTERM signal (UNIX), non-recoverable server error, etc.
// * closes the principal web server
func run() error {
rand.Seed(globaltime.Now().UnixNano())
// Load Configuration and defaults
cfg, err := loadConfiguration()
if err != nil {
if errors.Is(err, conf.ErrHelpWanted) {
return nil
}
return err
}
// Init logging
logger := logrus.New()
logger.SetOutput(os.Stdout)
if cfg.Debug {
logger.SetLevel(logrus.DebugLevel)
} else {
logger.SetLevel(logrus.InfoLevel)
}
logger.Infof("application initializing")
// Start Database
logger.Println("initializing database support")
dbconn, err := sql.Open("sqlite3", cfg.DB.Filename)
if err != nil {
logger.WithError(err).Error("error opening SQLite DB")
return fmt.Errorf("opening SQLite: %w", err)
}
defer func() {
logger.Debug("database stopping")
_ = dbconn.Close()
}()
db, err := database.New(dbconn)
if err != nil {
logger.WithError(err).Error("error creating AppDatabase")
return fmt.Errorf("creating AppDatabase: %w", err)
}
// Start (main) API server
logger.Info("initializing API server")
// Make a channel to listen for an interrupt or terminate signal from the OS.
// Use a buffered channel because the signal package requires it.
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
// Make a channel to listen for errors coming from the listener. Use a
// buffered channel so the goroutine can exit if we don't collect this error.
serverErrors := make(chan error, 1)
// Create the API router
apirouter, err := api.New(api.Config{
Logger: logger,
Database: db,
})
if err != nil {
logger.WithError(err).Error("error creating the API server instance")
return fmt.Errorf("creating the API server instance: %w", err)
}
router := apirouter.Handler()
router, err = registerWebUI(router)
if err != nil {
logger.WithError(err).Error("error registering web UI handler")
return fmt.Errorf("registering web UI handler: %w", err)
}
// Apply CORS policy
router = applyCORSHandler(router)
// Create the API server
apiserver := http.Server{
Addr: cfg.Web.APIHost,
Handler: router,
ReadTimeout: cfg.Web.ReadTimeout,
ReadHeaderTimeout: cfg.Web.ReadTimeout,
WriteTimeout: cfg.Web.WriteTimeout,
}
// Start the service listening for requests in a separate goroutine
go func() {
logger.Infof("API listening on %s", apiserver.Addr)
serverErrors <- apiserver.ListenAndServe()
logger.Infof("stopping API server")
}()
// Waiting for shutdown signal or POSIX signals
select {
case err := <-serverErrors:
// Non-recoverable server error
return fmt.Errorf("server error: %w", err)
case sig := <-shutdown:
logger.Infof("signal %v received, start shutdown", sig)
// Asking API server to shut down and load shed.
err := apirouter.Close()
if err != nil {
logger.WithError(err).Warning("graceful shutdown of apirouter error")
}
// Give outstanding requests a deadline for completion.
ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout)
defer cancel()
// Asking listener to shut down and load shed.
err = apiserver.Shutdown(ctx)
if err != nil {
logger.WithError(err).Warning("error during graceful shutdown of HTTP server")
err = apiserver.Close()
}
// Log the status of this shutdown.
switch {
case sig == syscall.SIGSTOP:
return errors.New("integrity issue caused shutdown")
case err != nil:
return fmt.Errorf("could not stop server gracefully: %w", err)
}
}
return nil
}

View file

@ -0,0 +1,12 @@
//go:build !webui
package main
import (
"net/http"
)
// registerWebUI is an empty stub because `webui` tag has not been specified.
func registerWebUI(hdl http.Handler) (http.Handler, error) {
return hdl, nil
}

View file

@ -0,0 +1,25 @@
//go:build webui
package main
import (
"fmt"
"git.sapienzaapps.it/fantasticcoffee/fantastic-coffee-decaffeinated/webui"
"io/fs"
"net/http"
"strings"
)
func registerWebUI(hdl http.Handler) (http.Handler, error) {
distDirectory, err := fs.Sub(webui.Dist, "dist")
if err != nil {
return nil, fmt.Errorf("error embedding WebUI dist/ directory: %w", err)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.RequestURI, "/dashboard/") {
http.StripPrefix("/dashboard/", http.FileServer(http.FS(distDirectory))).ServeHTTP(w, r)
return
}
hdl.ServeHTTP(w, r)
}), nil
}