initial commit

This commit is contained in:
Hyatt 2024-07-11 14:12:22 -05:00
commit 4eb5f4da67
Signed by: nhyatt
GPG Key ID: C50D0BBB5BC40BEA
13 changed files with 795 additions and 0 deletions

67
.gitignore vendored Normal file
View File

@ -0,0 +1,67 @@
# Application created directories
output/
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launce.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.history/
*.vsix
# GoLang
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
go.work
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk

58
.golangci.yaml Normal file
View File

@ -0,0 +1,58 @@
linters:
disable-all: true
enable:
# default linters
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
# project linters
- asasalint
- asciicheck
- bodyclose
- contextcheck
- dupl
- durationcheck
- errchkjson
- gocheckcompilerdirectives
- gocognit
- goconst
- gocritic
- godox
- goimports
- gosec
- grouper
- importas
- misspell
- musttag
- nestif
- nilerr
- nilnil
- prealloc
- reassign
- tagalign
- tenv
- unconvert
- unparam
- usestdlibvars
- wastedassign
- whitespace
fast: true
linter-settings:
tagalign:
order:
- json
- yaml
- yml
- toml
- mapstructure
- binding
- validate
- env
- default
- ignored
- required
- secret
- info

6
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"recommendations": [
"golang.go",
"oderwat.indent-rainbow"
]
}

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
# Build Container
####
FROM golang:alpine AS builder
COPY . /go/src/app
WORKDIR /go/src/app
RUN apk add --no-cache git && \
git config --global --add safe.directory /go/src/app && \
addgroup -S -g 1000 app && \
adduser --disabled-password -G app --gecos "application account" --home "/home/app" --shell "/sbin/nologin" --no-create-home --uid 1000 app && \
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags timetzdata -o go-webserver ./
# Step 3 - Running Container
####
FROM scratch
COPY --from=builder /etc/passwd /etc/group /etc/
COPY --from=builder --chown=app:app /go/src/app/go-webserver /app/go-webserver
USER app:app
WORKDIR /app/
ENTRYPOINT ["/app/go-webserver"]

6
assets/assets.go Normal file
View File

@ -0,0 +1,6 @@
package assets
import "embed"
//go:embed html/*
var EmbedData embed.FS

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ .Title }}</title>
<style type="text/css">
body {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
font-family: 'Poppins', sans-serif;
background-image: linear-gradient(45deg, #f6d200 25%, #181617 25%, #181617 50%, #f6d200 50%, #f6d200 75%, #181617 75%, #181617 100%);
}
h1 {
text-transform: uppercase;
background: repeating-linear-gradient(
45deg,
#f6d200 ,
#f6d200 10px,
#181617 10px,
#181617 20px
);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 384px;
margin: 0;
line-height: .7;
position: relative;
}
</style>
</head>
<body id="home">
<h1 class="text"><span>{{ printf "%d" .ErrorCode }}</span></h1>
</body>
</html>

BIN
assets/html/images/game.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

15
assets/html/index.tplt Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ .Title }}</title>
</head>
<body>
<div style="text-align: center;">
Time: {{ .Time }}
<br>
<img src="/images/game.png" style="height: 150px;">
</div>
</body>
</html>

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module example.com/go-performance
go 1.22.5

View File

@ -0,0 +1,241 @@
package config
import (
"flag"
"fmt"
"os"
"reflect"
"strconv"
"strings"
)
type structInfo struct {
Name string
Alt string
Info string
Key string
Field reflect.Value
Tags reflect.StructTag
Type reflect.Type
DefaultValue interface{}
Secret interface{}
}
func getEnv[t string | bool | int | int64 | float64](env string, def t) (t, error) {
val := os.Getenv(env)
if len(val) == 0 {
return def, nil
}
output := *new(t)
switch (interface{})(def).(type) {
case string:
v, err := typeConversion("string", val)
if err != nil {
return (interface{})(false).(t), err
}
output = v.(t)
case bool:
v, err := typeConversion("bool", val)
if err != nil {
return (interface{})(false).(t), err
}
output = v.(t)
case int:
v, err := typeConversion("int", val)
if err != nil {
return (interface{})(int(0)).(t), err
}
output = (interface{})(int(v.(int64))).(t)
case int64:
v, err := typeConversion("int64", val)
if err != nil {
return (interface{})(int64(0)).(t), err
}
output = v.(t)
case float64:
v, err := typeConversion("float64", val)
if err != nil {
return (interface{})(float64(0)).(t), err
}
output = v.(t)
}
return output, nil
}
func getStructInfo(spec interface{}) ([]structInfo, error) {
s := reflect.ValueOf(spec)
if s.Kind() != reflect.Pointer {
return []structInfo{}, fmt.Errorf("getStructInfo() was sent a %s instead of a pointer to a struct.\n", s.Kind())
}
s = s.Elem()
if s.Kind() != reflect.Struct {
return []structInfo{}, fmt.Errorf("getStructInfo() was sent a %s instead of a struct.\n", s.Kind())
}
typeOfSpec := s.Type()
infos := make([]structInfo, 0, s.NumField())
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
ftype := typeOfSpec.Field(i)
ignored, _ := strconv.ParseBool(ftype.Tag.Get("ignored"))
if !f.CanSet() || ignored {
continue
}
for f.Kind() == reflect.Pointer {
if f.IsNil() {
if f.Type().Elem().Kind() != reflect.Struct {
break
}
f.Set(reflect.New(f.Type().Elem()))
}
f = f.Elem()
}
secret, err := typeConversion(ftype.Type.String(), ftype.Tag.Get("secret"))
if err != nil {
secret = false
}
var desc string
if len(ftype.Tag.Get("info")) != 0 {
desc = fmt.Sprintf("(%s) %s", strings.ToUpper(ftype.Tag.Get("env")), ftype.Tag.Get("info"))
} else {
desc = fmt.Sprintf("(%s)", strings.ToUpper(ftype.Tag.Get("env")))
}
info := structInfo{
Name: ftype.Name,
Alt: strings.ToUpper(ftype.Tag.Get("env")),
Info: desc,
Key: ftype.Name,
Field: f,
Tags: ftype.Tag,
Type: ftype.Type,
Secret: secret,
}
if info.Alt != "" {
info.Key = info.Alt
}
info.Key = strings.ToUpper(info.Key)
if ftype.Tag.Get("default") != "" {
v, err := typeConversion(ftype.Type.String(), ftype.Tag.Get("default"))
if err != nil {
return []structInfo{}, err
}
info.DefaultValue = v
}
infos = append(infos, info)
}
return infos, nil
}
func typeConversion(t, v string) (interface{}, error) {
switch t {
case "string": //nolint:goconst
return v, nil
case "int": //nolint:goconst
return strconv.ParseInt(v, 10, 0)
case "int8":
return strconv.ParseInt(v, 10, 8)
case "int16":
return strconv.ParseInt(v, 10, 16)
case "int32":
return strconv.ParseInt(v, 10, 32)
case "int64":
return strconv.ParseInt(v, 10, 64)
case "uint":
return strconv.ParseUint(v, 10, 0)
case "uint16":
return strconv.ParseUint(v, 10, 16)
case "uint32":
return strconv.ParseUint(v, 10, 32)
case "uint64":
return strconv.ParseUint(v, 10, 64)
case "float32":
return strconv.ParseFloat(v, 32)
case "float64":
return strconv.ParseFloat(v, 64)
case "complex64":
return strconv.ParseComplex(v, 64)
case "complex128":
return strconv.ParseComplex(v, 128)
case "bool": //nolint:goconst
return strconv.ParseBool(v)
}
return nil, fmt.Errorf("Unable to identify type.")
}
func (cfg *Config) parseFlags(cfgInfo []structInfo) error { //nolint:gocognit
for _, info := range cfgInfo {
switch info.Type.String() {
case "string":
var dv string
if info.DefaultValue != nil {
dv = info.DefaultValue.(string)
}
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string)
retVal, err := getEnv(info.Alt, dv)
if err != nil {
return err
}
flag.StringVar(p, info.Name, retVal, info.Info)
case "bool":
var dv bool
if info.DefaultValue != nil {
dv = info.DefaultValue.(bool)
}
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool)
retVal, err := getEnv(info.Alt, dv)
if err != nil {
return err
}
flag.BoolVar(p, info.Name, retVal, info.Info)
case "int":
var dv int
if info.DefaultValue != nil {
dv = int(info.DefaultValue.(int64))
}
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int)
retVal, err := getEnv(info.Alt, dv)
if err != nil {
return err
}
flag.IntVar(p, info.Name, retVal, info.Info)
case "int64":
var dv int64
if info.DefaultValue != nil {
dv = info.DefaultValue.(int64)
}
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int64)
retVal, err := getEnv(info.Alt, dv)
if err != nil {
return err
}
flag.Int64Var(p, info.Name, retVal, info.Info)
case "float64":
var dv float64
if info.DefaultValue != nil {
dv = info.DefaultValue.(float64)
}
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*float64)
retVal, err := getEnv(info.Alt, dv)
if err != nil {
return err
}
flag.Float64Var(p, info.Name, retVal, info.Info)
}
}
flag.Parse()
return nil
}

View File

@ -0,0 +1,38 @@
package config
import (
"log"
"os"
"time"
)
func Init() Config {
cfg := New()
cfgInfo, err := getStructInfo(&cfg)
if err != nil {
log.Fatalf("Unable to initialize program: %v", err)
}
// get command line flags
if err := cfg.parseFlags(cfgInfo); err != nil {
log.Fatalf("Unable to initialize program: %v", err)
}
// set logging Level
setLogLevel(&cfg)
// set timezone & time format
cfg.TZUTC, _ = time.LoadLocation("UTC")
cfg.TZLocal, err = time.LoadLocation(cfg.TimeZoneLocal)
if err != nil {
cfg.Log.Error("Unable to parse timezone string", "error", err)
os.Exit(1)
}
// print running config
printRunningConfig(&cfg, cfgInfo)
// return configuration
return cfg
}

View File

@ -0,0 +1,81 @@
package config
import (
"log/slog"
"os"
"reflect"
"strconv"
"time"
)
type Config struct {
// time configuration
TimeFormat string `default:"2006-01-02 15:04:05" env:"time_format"`
TimeZoneLocal string `default:"America/Chicago" env:"time_zone"`
TZLocal *time.Location `ignored:"true"`
TZUTC *time.Location `ignored:"true"`
// logging
LogLevel int `default:"50" env:"log_level"`
Log *slog.Logger `ignored:"true"`
SLogLevel *slog.LevelVar `ignored:"true"`
// webserver
WebServerPort int `default:"8080" env:"webserver_port"`
WebServerIP string `default:"0.0.0.0" env:"webserver_ip"`
WebServerReadTimeout int `default:"5" env:"webserver_read_timeout"`
WebServerWriteTimeout int `default:"1" env:"webserver_write_timeout"`
WebServerIdleTimeout int `default:"2" env:"webserver_idle_timeout"`
}
// New initializes the config variable for use with a prepared set of defaults.
func New() Config {
cfg := Config{
SLogLevel: new(slog.LevelVar),
}
cfg.Log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: cfg.SLogLevel,
}))
return cfg
}
func setLogLevel(cfg *Config) {
switch {
// error
case cfg.LogLevel <= 20:
cfg.SLogLevel.Set(slog.LevelError)
cfg.Log.Info("Log level updated", "level", slog.LevelError)
// warning
case cfg.LogLevel > 20 && cfg.LogLevel <= 40:
cfg.SLogLevel.Set(slog.LevelWarn)
cfg.Log.Info("Log level updated", "level", slog.LevelWarn)
// info
case cfg.LogLevel > 40 && cfg.LogLevel <= 60:
cfg.SLogLevel.Set(slog.LevelInfo)
cfg.Log.Info("Log level updated", "level", slog.LevelInfo)
// debug
case cfg.LogLevel > 60:
cfg.SLogLevel.Set(slog.LevelDebug)
cfg.Log.Info("Log level updated", "level", slog.LevelDebug)
}
// set default logger
slog.SetDefault(cfg.Log)
}
func printRunningConfig(cfg *Config, cfgInfo []structInfo) {
for _, info := range cfgInfo {
switch info.Type.String() {
case "string":
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string)
cfg.Log.Debug("Running Configuration", info.Alt, *p)
case "bool":
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool)
cfg.Log.Debug("Running Configuration", info.Alt, strconv.FormatBool(*p))
case "int":
p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int)
cfg.Log.Debug("Running Configuration", info.Alt, strconv.FormatInt(int64(*p), 10))
}
}
}

215
main.go Normal file
View File

@ -0,0 +1,215 @@
package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"html/template"
"log/slog"
"net/http"
"os"
"os/signal"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"example.com/go-performance/assets"
"example.com/go-performance/internal/config"
)
const (
cTcss string = "text/css"
cTxpem string = "application/x-pem-file"
cTpdf string = "application/pdf"
cTmpeg string = "audio/mpeg"
cTwoff string = "font/woff"
cTwoff2 string = "font/woff2"
cTpng string = "image/png"
cTjpeg string = "image/jpg"
cTjs string = "text/javascript"
cTjson string = "application/json"
cTplain string = "text/plain"
cTraw string = "text/raw"
cThtml string = "text/html"
)
type webErrStruct struct {
Error bool `json:"error" yaml:"error"`
ErrorMsg string `json:"error_message" yaml:"errorMessage"`
}
var (
validFiles map[string]string = map[string]string{
"/images/game.png": cTpng,
}
cfg config.Config
)
func forever(log *slog.Logger) {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
sig := <-c
log.Warn("Shutting down, triggered by signal", "signal", sig)
}
func main() {
// initialize all parameters
cfg = config.Init()
// configure shutdown sequence
defer func() {
cfg.Log.Info("Shutdown sequence complete")
}()
// start webserver
go httpServer(cfg.Log)
forever(cfg.Log)
}
func httpServer(log *slog.Logger) {
path := http.NewServeMux()
connection := &http.Server{
Addr: cfg.WebServerIP + ":" + strconv.FormatInt(int64(cfg.WebServerPort), 10),
Handler: path,
ReadTimeout: time.Duration(cfg.WebServerReadTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.WebServerWriteTimeout) * time.Second,
IdleTimeout: time.Duration(cfg.WebServerIdleTimeout) * time.Second,
}
path.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { webRoot(log, w, r) })
if err := connection.ListenAndServe(); err != nil {
log.Error("Unable to create webserver", "function", "httpServer", "error", err)
panic(err)
}
}
func isValidReq(file string) (string, error) {
for f, t := range validFiles {
if file == f {
return t, nil
}
}
return "", fmt.Errorf("Invalid file requested: %s", file)
}
func webRoot(log *slog.Logger, w http.ResponseWriter, r *http.Request) {
httpAccessLog(log, r)
if strings.ToLower(r.Method) != "get" {
log.Debug("Request made using the wrong method", "function", "webRoot", "url", r.URL.Path, "expected-method", "GET", "requested-method", r.Method)
tmpltError(log, w, http.StatusBadRequest, "Invalid http method.")
return
}
if r.URL.Path == "/" {
tmpltWebRoot(log, w)
return
}
cType, err := isValidReq(r.URL.Path)
if err != nil {
log.Debug("Request not found", "function", "webRoot", "url", r.URL.Path)
tmpltStatusNotFound(log, w, r.URL.Path)
return
}
w.Header().Add("Content-Type", cType)
o, err := assets.EmbedData.ReadFile("html" + r.URL.Path)
if err != nil {
log.Error("Unable to read local embedded file", "function", "webRoot", "error", err)
tmpltError(log, w, http.StatusInternalServerError, "Server unable to retrieve file data.")
return
}
if regexp.MustCompile(`gzip`).Match([]byte(r.Header.Get("Accept-Encoding"))) {
w.Header().Add("Content-Encoding", "gzip")
gw := gzip.NewWriter(w)
defer gw.Close()
gw.Write(o) //nolint: errcheck
} else {
w.Write(o) //nolint: errcheck
}
}
func httpAccessLog(log *slog.Logger, req *http.Request) {
log.Debug("AccessLog", "method", req.Method, "remote-addr", req.RemoteAddr, "uri", req.RequestURI)
}
func tmpltWebRoot(log *slog.Logger, w http.ResponseWriter) {
tmplt, err := template.ParseFS(
assets.EmbedData,
"html/index.tplt",
)
if err != nil {
log.Debug("Unable to parse HTML template", "function", "tmpltWebRoot", "error", err)
tmpltError(log, w, http.StatusInternalServerError, "Template Parse Error.")
return
}
var msgBuffer bytes.Buffer
if err := tmplt.Execute(&msgBuffer, struct {
Time string
Title string
Version string
}{
Time: time.Now().In(cfg.TZLocal).Format(cfg.TimeFormat),
Title: "Performance Test",
Version: "v1.0.0",
}); err != nil {
log.Debug("Unable to execute HTML template", "function", "tmpltWebRoot", "error", err)
tmpltError(log, w, http.StatusInternalServerError, "Template Parse Error.")
return
}
w.Write(msgBuffer.Bytes()) //nolint: errcheck
}
func tmpltError(log *slog.Logger, w http.ResponseWriter, serverStatus int, message string) {
var (
output []byte
o = webErrStruct{
Error: true,
ErrorMsg: message,
}
err error
)
w.Header().Add("Content-Type", "application/json")
output, err = json.MarshalIndent(o, "", " ")
if err != nil {
log.Warn("Unable to marshal error", "function", "tmpltError", "error", err)
w.WriteHeader(serverStatus)
w.Write(output) //nolint:errcheck
}
}
func tmpltStatusNotFound(log *slog.Logger, w http.ResponseWriter, path string) {
tmplt, err := template.ParseFS(assets.EmbedData, "html/file-not-found.tplt")
if err != nil {
log.Debug("Unable to parse HTML template", "function", "tmpltStatusNotFound", "error", err)
tmpltError(log, w, http.StatusInternalServerError, "Template Parse Error.")
return
}
var msgBuffer bytes.Buffer
if err := tmplt.Execute(&msgBuffer, struct {
Title string
ErrorCode int
}{
Title: path,
ErrorCode: http.StatusNotFound,
}); err != nil {
log.Debug("Unable to execute HTML template", "function", "tmpltStatusNotFound", "error", err)
tmpltError(log, w, http.StatusInternalServerError, "Template Parse Error.")
return
}
w.Write(msgBuffer.Bytes()) //nolint: errcheck
}