Add template

Signed-off-by: AKP <tom@tdpain.net>
This commit is contained in:
akp 2022-07-16 19:52:59 +01:00
commit 14c167d564
No known key found for this signature in database
GPG key ID: AA5726202C8879B7
12 changed files with 487 additions and 0 deletions

View file

@ -0,0 +1,21 @@
package config
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
)
func InitLogging() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
}
var HTTP = struct {
Host string
Port int
Secure bool
}{
Host: asString(withDefault(fetchFromFile("http.host"), "0.0.0.0")),
Port: asInt(withDefault(fetchFromFile("http.port"), 8080)),
Secure: asBool(required(fetchFromFile("http.secure"))),
}

View file

@ -0,0 +1,96 @@
package config
import (
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"io/ioutil"
"regexp"
"strconv"
"strings"
)
const configFileName = "config.yml"
var (
rawConfigFileContents map[string]any
lastKey string
)
func loadConfigFileFromDisk() {
if rawConfigFileContents != nil {
return
}
fcont, err := ioutil.ReadFile(configFileName)
if err != nil {
log.Fatal().Err(err).Msgf("failed to load file %s", configFileName)
}
rawConfigFileContents = make(map[string]any)
if err := yaml.Unmarshal(fcont, &rawConfigFileContents); err != nil {
log.Fatal().Err(err).Msg("could not unmarshal config file")
}
}
type optionalItem struct {
item any
found bool
}
var indexedPartRegexp = regexp.MustCompile(`(?m)([a-zA-Z]+)(?:\[(\d+)\])?`)
func fetchFromFile(key string) optionalItem {
// http[2].bananas
loadConfigFileFromDisk()
lastKey = key
parts := strings.Split(key, ".")
var cursor any = rawConfigFileContents
for _, part := range parts {
components := indexedPartRegexp.FindStringSubmatch(part)
key := components[1]
index, _ := strconv.ParseInt(components[2], 10, 32)
isIndexed := components[2] != ""
item, found := cursor.(map[string]any)[key]
if !found {
return optionalItem{nil, false}
}
if isIndexed {
arr, conversionOk := item.([]any)
if !conversionOk {
log.Fatal().Msgf("attempted to index non-indexable item %s", key)
}
cursor = arr[index]
} else {
cursor = item
}
}
return optionalItem{cursor, true}
}
func required(opt optionalItem) any {
if !opt.found {
log.Fatal().Msgf("required key %s not found", lastKey)
}
return opt.item
}
func withDefault(opt optionalItem, defaultValue any) any {
if !opt.found {
return defaultValue
}
return opt.item
}
func asInt(x any) int {
return x.(int)
}
func asString(x any) string {
return x.(string)
}
func asBool(x any) bool {
return x.(bool)
}

71
application/db/db.go Normal file
View file

@ -0,0 +1,71 @@
package db
import (
"context"
"database/sql"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"math"
"net"
"time"
)
type DB struct {
pool *sql.DB
ContextTimeout time.Duration
}
const maxConnectionAttempts = 4
func New() (*DB, error) {
// TODO: Setup DSN and database driver
dsn := ""
log.Info().Msg("connecting to database")
db, err := sql.Open("postgres", dsn) // TODO: This
if err != nil {
return nil, errors.Wrap(err, "could not open SQL connection")
}
rtn := &DB{
pool: db,
ContextTimeout: time.Second,
}
for i := 1; i <= maxConnectionAttempts; i += 1 {
logger := log.With().Int("attempt", i).Int("maxAttempts", maxConnectionAttempts).Logger()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
err := rtn.pool.PingContext(ctx)
if err == nil {
cancel()
break
}
if e, ok := err.(*net.OpError); ((ok && e.Op == "dial") || errors.Is(err, context.DeadlineExceeded)) && i != maxConnectionAttempts {
cancel()
retryIn := int(math.Pow(math.E, float64(i)))
logger.Warn().Err(err).Msgf("could not connect to database - retrying in %d seconds", retryIn)
time.Sleep(time.Second * time.Duration(retryIn))
continue
}
cancel()
return nil, errors.Wrapf(err, "could not ping database after %d attempts", i)
}
return rtn, nil
}
func (db *DB) newContext() (context.Context, func()) {
return context.WithTimeout(context.Background(), db.ContextTimeout)
}
func smartRollback(tx *sql.Tx) {
err := tx.Rollback()
if err != nil && !errors.Is(err, sql.ErrTxDone) {
log.Warn().Stack().Err(errors.WithStack(err)).Str("location", "smartRollback").Msg("failed to rollback transaction")
}
}

View file

@ -0,0 +1,80 @@
package db
import (
"database/sql"
_ "embed"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
var migrationFunctions = []func(trans *sql.Tx) error{
migrate0to1,
}
func (db *DB) Migrate() error {
log.Info().Msg("running migrations")
// list tables
tx, err := db.pool.Begin()
if err != nil {
return errors.WithMessage(err, "could not begin transaction")
}
defer smartRollback(tx)
rows, err := db.pool.Query(`SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = 'public';`)
if err != nil {
return errors.WithStack(err)
}
defer rows.Close()
existingTables := make(map[string]struct{})
for rows.Next() {
var tableName string
if err := rows.Scan(&tableName); err != nil {
return errors.WithStack(err)
}
existingTables[tableName] = struct{}{}
}
var databaseVersion int
if _, found := existingTables["version"]; found {
err := db.pool.QueryRow(`SELECT "version" FROM "version";`).Scan(&databaseVersion)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return errors.WithStack(err)
}
}
if x := len(migrationFunctions); databaseVersion == x {
log.Info().Msg("migrations up-to-date without any changes")
return nil
} else if databaseVersion > x {
return errors.New("corrupt database: database version too high")
}
for _, f := range migrationFunctions[databaseVersion:] {
if err := f(tx); err != nil {
return errors.WithStack(err)
}
}
log.Info().Msg("committing migrations")
return errors.WithStack(
tx.Commit(),
)
}
//go:embed migrations/0to1.sql
var migrate0to1SQL string
func migrate0to1(trans *sql.Tx) error {
log.Info().Msg("migrating new database to v1")
_, err := trans.Exec(migrate0to1SQL)
if err != nil {
return errors.Wrap(err, "failed to migrate database version from v0 to v1")
}
return nil
}

View file

@ -0,0 +1,9 @@
-- Migrate a new database to v1 format.
CREATE TABLE "version"
(
version INT
);
INSERT INTO "version"(version)
VALUES (1);

View file

@ -0,0 +1,28 @@
package endpoints
import (
"github.com/codemicro/go-fiber-sql/application/db"
"github.com/codemicro/go-fiber-sql/application/urls"
"github.com/codemicro/go-fiber-sql/application/util"
"github.com/gofiber/fiber/v2"
)
type Endpoints struct {
db *db.DB
}
func New(dbi *db.DB) *Endpoints {
return &Endpoints{
db: dbi,
}
}
func (e *Endpoints) SetupApp() *fiber.App {
app := fiber.New(fiber.Config{
ErrorHandler: util.JSONErrorHandler,
})
app.Get(urls.Index, e.Index)
return app
}

View file

@ -0,0 +1,7 @@
package endpoints
import "github.com/gofiber/fiber/v2"
func (e *Endpoints) Index(ctx *fiber.Ctx) error {
return ctx.SendString("Hello world!")
}

43
application/main.go Normal file
View file

@ -0,0 +1,43 @@
package main
import (
"fmt"
"github.com/codemicro/go-fiber-sql/application/config"
"github.com/codemicro/go-fiber-sql/application/db"
"github.com/codemicro/go-fiber-sql/application/endpoints"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"strconv"
)
func run() error {
database, err := db.New()
if err != nil {
return errors.WithStack(err)
}
if err := database.Migrate(); err != nil {
return errors.Wrap(err, "failed migration")
}
e := endpoints.New(nil)
app := e.SetupApp()
serveAddr := config.HTTP.Host + ":" + strconv.Itoa(config.HTTP.Port)
log.Info().Msgf("starting server on %s", serveAddr)
if err := app.Listen(serveAddr); err != nil {
return errors.Wrap(err, "fiber server run failed")
}
return nil
}
func main() {
config.InitLogging()
if err := run(); err != nil {
fmt.Printf("%+v\n", err)
log.Error().Stack().Err(err).Msg("failed to run coordinator")
}
}

5
application/urls/urls.go Normal file
View file

@ -0,0 +1,5 @@
package urls
const (
Index = "/"
)

View file

@ -0,0 +1,63 @@
package util
import (
"encoding/json"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
)
type RichError struct {
Status int
Reason string
Detail any
}
func NewRichError(status int, reason string, detail any) error {
return &RichError{
Status: status,
Reason: reason,
Detail: detail,
}
}
func NewRichErrorFromFiberError(err *fiber.Error, detail any) error {
return NewRichError(err.Code, err.Message, detail)
}
func (r *RichError) Error() string {
return fmt.Sprintf("handler error, %d: %s", r.Status, r.Reason)
}
func (r *RichError) AsJSON() ([]byte, error) {
info := map[string]any{
"status": "error",
"message": r.Reason,
}
if r.Detail != nil {
info["detail"] = r.Detail
}
return json.Marshal(info)
}
func JSONErrorHandler(ctx *fiber.Ctx, err error) error {
var re *RichError
if e, ok := err.(*fiber.Error); ok {
re = NewRichErrorFromFiberError(e, nil).(*RichError)
} else if e, ok := err.(*RichError); ok {
re = e
} else {
log.Error().Stack().Err(err).Str("location", "fiber error handler").Str("route", ctx.OriginalURL()).Send()
re = NewRichErrorFromFiberError(fiber.ErrInternalServerError, nil).(*RichError)
}
jsonBytes, err := re.AsJSON()
if err != nil {
jsonBytes = []byte(`{"status":"error","message":"Internal Server Error","detail":"unable to produce detailed description"}`)
log.Error().Err(err).Str("location", "fiber error handler").Msg("unable to produce error response")
}
ctx.Status(re.Status)
ctx.Type("json")
return ctx.Send(jsonBytes)
}

21
go.mod Normal file
View file

@ -0,0 +1,21 @@
module github.com/codemicro/go-fiber-sql
go 1.18
require (
github.com/gofiber/fiber/v2 v2.35.0
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.27.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.38.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
)

43
go.sum Normal file
View file

@ -0,0 +1,43 @@
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.35.0 h1:ct+jKw8Qb24WEIZx3VV3zz9VXyBZL7mcEjNaqj3g0h0=
github.com/gofiber/fiber/v2 v2.35.0/go.mod h1:tgCr+lierLwLoVHHO/jn3Niannv34WRkQETU8wiL9fQ=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.38.0 h1:yTjSSNjuDi2PPvXY2836bIwLmiTS2T4T9p1coQshpco=
github.com/valyala/fasthttp v1.38.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=