From 4eb5f4da673d9bae58abd430e35efa7b4ea50b59 Mon Sep 17 00:00:00 2001 From: nhyatt Date: Thu, 11 Jul 2024 14:12:22 -0500 Subject: [PATCH] initial commit --- .gitignore | 67 +++++++++ .golangci.yaml | 58 ++++++++ .vscode/extensions.json | 6 + Dockerfile | 25 ++++ assets/assets.go | 6 + assets/html/file-not-found.tplt | 40 +++++ assets/html/images/game.png | Bin 0 -> 31609 bytes assets/html/index.tplt | 15 ++ go.mod | 3 + internal/config/envconfig.go | 241 +++++++++++++++++++++++++++++++ internal/config/initialize.go | 38 +++++ internal/config/struct-config.go | 81 +++++++++++ main.go | 215 +++++++++++++++++++++++++++ 13 files changed, 795 insertions(+) create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 .vscode/extensions.json create mode 100644 Dockerfile create mode 100644 assets/assets.go create mode 100644 assets/html/file-not-found.tplt create mode 100644 assets/html/images/game.png create mode 100644 assets/html/index.tplt create mode 100644 go.mod create mode 100644 internal/config/envconfig.go create mode 100644 internal/config/initialize.go create mode 100644 internal/config/struct-config.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e689df1 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..7a02377 --- /dev/null +++ b/.golangci.yaml @@ -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 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..bb0dba0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "golang.go", + "oderwat.indent-rainbow" + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9fce29d --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/assets/assets.go b/assets/assets.go new file mode 100644 index 0000000..3ed61af --- /dev/null +++ b/assets/assets.go @@ -0,0 +1,6 @@ +package assets + +import "embed" + +//go:embed html/* +var EmbedData embed.FS diff --git a/assets/html/file-not-found.tplt b/assets/html/file-not-found.tplt new file mode 100644 index 0000000..ccf7693 --- /dev/null +++ b/assets/html/file-not-found.tplt @@ -0,0 +1,40 @@ + + + + + {{ .Title }} + + + + +

{{ printf "%d" .ErrorCode }}

+ + \ No newline at end of file diff --git a/assets/html/images/game.png b/assets/html/images/game.png new file mode 100644 index 0000000000000000000000000000000000000000..47b714b2a681301ae681b42e2b5ad1536f43dfcd GIT binary patch literal 31609 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 ZnwAisO?`1QdX@3Ue~Wrcx^Cy8{|3PHr?vn9 literal 0 HcmV?d00001 diff --git a/assets/html/index.tplt b/assets/html/index.tplt new file mode 100644 index 0000000..4ab7bde --- /dev/null +++ b/assets/html/index.tplt @@ -0,0 +1,15 @@ + + + + + {{ .Title }} + + + +
+ Time: {{ .Time }} +
+ +
+ + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c0eeb53 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module example.com/go-performance + +go 1.22.5 diff --git a/internal/config/envconfig.go b/internal/config/envconfig.go new file mode 100644 index 0000000..73c1786 --- /dev/null +++ b/internal/config/envconfig.go @@ -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 +} diff --git a/internal/config/initialize.go b/internal/config/initialize.go new file mode 100644 index 0000000..16ee1a3 --- /dev/null +++ b/internal/config/initialize.go @@ -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 +} diff --git a/internal/config/struct-config.go b/internal/config/struct-config.go new file mode 100644 index 0000000..f499a77 --- /dev/null +++ b/internal/config/struct-config.go @@ -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)) + } + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..98d1c9b --- /dev/null +++ b/main.go @@ -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 +}