From e0277463ea198d113c6d6df2d131988dd65184fc Mon Sep 17 00:00:00 2001 From: kodehat Date: Fri, 23 Feb 2024 18:05:01 +0100 Subject: [PATCH] feat: prepare tests, restructure templates and http mux --- Dockerfile | 2 +- config.yml | 3 +- internal/components/layouts.templ | 28 +++++----- internal/components/partials.templ | 6 ++- internal/components/portal.templ | 26 ++++----- internal/config/config.go | 17 +++--- internal/routes/home_handler.go | 30 ----------- internal/routes/routes.go | 41 -------------- internal/routes/version_handler.go | 24 --------- internal/server/health_handler.go | 10 ++++ internal/server/home_handler.go | 21 ++++++++ internal/{routes => server}/page_handler.go | 14 +---- .../{routes => server}/portals_handler.go | 13 +++-- internal/{routes => server}/rest_handler.go | 2 +- internal/server/routes.go | 31 +++++++++++ internal/server/server.go | 20 +++++++ internal/{routes => server}/static_handler.go | 2 +- internal/server/version_handler.go | 14 +++++ internal/utils/http.go | 7 --- internal/utils/test.go | 53 +++++++++++++++++++ main.go | 46 +++++++++++++--- main_test.go | 34 ++++++++++++ 22 files changed, 278 insertions(+), 166 deletions(-) delete mode 100644 internal/routes/home_handler.go delete mode 100644 internal/routes/routes.go delete mode 100644 internal/routes/version_handler.go create mode 100644 internal/server/health_handler.go create mode 100644 internal/server/home_handler.go rename internal/{routes => server}/page_handler.go (55%) rename internal/{routes => server}/portals_handler.go (57%) rename internal/{routes => server}/rest_handler.go (97%) create mode 100644 internal/server/routes.go create mode 100644 internal/server/server.go rename internal/{routes => server}/static_handler.go (92%) create mode 100644 internal/server/version_handler.go delete mode 100644 internal/utils/http.go create mode 100644 internal/utils/test.go create mode 100644 main_test.go diff --git a/Dockerfile b/Dockerfile index 95d0fcb..0129e0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ COPY internal internal/ RUN apk add --no-cache git bash RUN go install github.com/a-h/templ/cmd/templ@latest && templ generate -RUN sh build.sh $VERSION +RUN sh build.sh "$VERSION" FROM alpine:3.19.1 diff --git a/config.yml b/config.yml index b6a00d9..101f6cb 100644 --- a/config.yml +++ b/config.yml @@ -1,7 +1,8 @@ host: localhost port: 3000 title: "portkey" -footerText: "Works like a portal." +footer: |- +

made with ☕️ by CodeHat

sortAlphabetically: false portals: - title: 1password diff --git a/internal/components/layouts.templ b/internal/components/layouts.templ index 7c08873..e35956e 100644 --- a/internal/components/layouts.templ +++ b/internal/components/layouts.templ @@ -1,6 +1,8 @@ package components -templ Base(title string) { +import "github.com/kodehat/portkey/internal/config" + +templ Base(pageTitle string, config config.Config) { @@ -21,7 +23,7 @@ templ Base(title string) { - { title } + { config.Title } @@ -48,27 +50,27 @@ templ Base(title string) { -

portkey

+

{ config.Title }

{ children... } } -templ HomeLayout(title string, contents templ.Component) { - @Base(title) { +templ HomeLayout(pageTitle string, config config.Config, contents templ.Component) { + @Base(pageTitle, config) { } } -templ ContentLayout(title string, header string, contents templ.Component, portals []templ.Component, footerText string) { - @Base(title) { +templ ContentLayout(pageTitle string, config config.Config, contents templ.Component) { + @Base(pageTitle, config) {
-

{ header }

+

{ pageTitle }

@@ -77,9 +79,9 @@ templ ContentLayout(title string, header string, contents templ.Component, porta
} } diff --git a/internal/components/partials.templ b/internal/components/partials.templ index 0ea5152..52bc9b7 100644 --- a/internal/components/partials.templ +++ b/internal/components/partials.templ @@ -1,7 +1,9 @@ package components -templ PortalPartial(portals []templ.Component) { +import "github.com/kodehat/portkey/internal/models" + +templ PortalPartial(portals []models.Portal) { for _, portal := range portals { - @portal + @HomePortal(portal) } } diff --git a/internal/components/portal.templ b/internal/components/portal.templ index 905d384..f990c13 100644 --- a/internal/components/portal.templ +++ b/internal/components/portal.templ @@ -1,33 +1,35 @@ package components -templ HomePortal(link string, emoji string, title string, external bool) { +import "github.com/kodehat/portkey/internal/models" + +templ HomePortal(portal models.Portal) { - if emoji != "" { - { emoji } + if portal.Emoji != "" { + { portal.Emoji } } { title } + >{ portal.Title } } -templ FooterPortal(link string, emoji string, title string, external bool) { +templ FooterPortal(portal models.Portal) { - if emoji != "" { - { emoji } + if portal.Emoji != "" { + { portal.Emoji } } { title } + >{ portal.Title } } diff --git a/internal/config/config.go b/internal/config/config.go index 0c7335b..7866f31 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "log" + "os" "path/filepath" "sort" "strings" @@ -14,9 +15,9 @@ import ( type Config struct { Host string - Port int + Port string Title string - FooterText string + Footer string SortAlphabetically bool Portals []models.Portal Pages []models.Page @@ -29,8 +30,8 @@ type Flags struct { var C Config var F Flags -func init() { - loadFlags() +func Load() { + LoadFlags() configPath, err := filepath.Abs(F.ConfigPath) if err != nil { panic(fmt.Errorf("fatal error config file: %w", err)) @@ -39,9 +40,13 @@ func init() { loadConfig(F.ConfigPath) } -func loadFlags() { +func LoadFlags() { var configPath string - flag.StringVar(&configPath, "config-path", ".", "path where config.yml can be found") + workDir, err := os.Getwd() + if err != nil { + workDir = "." + } + flag.StringVar(&configPath, "config-path", workDir, "path where config.yml can be found") flag.Parse() F = Flags{ ConfigPath: configPath, diff --git a/internal/routes/home_handler.go b/internal/routes/home_handler.go deleted file mode 100644 index f15e393..0000000 --- a/internal/routes/home_handler.go +++ /dev/null @@ -1,30 +0,0 @@ -package routes - -import ( - "net/http" - "sync" - - "github.com/a-h/templ" - "github.com/kodehat/portkey/internal/components" - "github.com/kodehat/portkey/internal/config" - "github.com/kodehat/portkey/internal/utils" -) - -func homeHandler() http.HandlerFunc { - var ( - init sync.Once - allFooterPortals []templ.Component - ) - init.Do(func() { - allFooterPortals = getAllFooterPortals() - }) - home := components.HomePage() - return func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - w.WriteHeader(http.StatusNotFound) - templ.Handler(components.ContentLayout(utils.PageTitle("404 Not Found", config.C.Title), "404 Not Found", components.NotFound(), allFooterPortals, config.C.FooterText)).ServeHTTP(w, r) - return - } - templ.Handler(components.HomeLayout(config.C.Title, home)).ServeHTTP(w, r) - } -} diff --git a/internal/routes/routes.go b/internal/routes/routes.go deleted file mode 100644 index 07ae3fc..0000000 --- a/internal/routes/routes.go +++ /dev/null @@ -1,41 +0,0 @@ -package routes - -import ( - "embed" - "net/http" - - "github.com/a-h/templ" - "github.com/kodehat/portkey/internal/components" - "github.com/kodehat/portkey/internal/config" -) - -func AddRoutes(mux *http.ServeMux, static embed.FS) { - // Home - mux.HandleFunc("/", homeHandler()) - - // Custom pages - for _, pageHandler := range pageHandler() { - mux.HandleFunc(pageHandler.pagePath, pageHandler.handlerFunc) - } - - // Fix pages - mux.HandleFunc("/version", versionHandler()) - - // Static - mux.HandleFunc("/static/", staticHandler(static)) - - // htmx - mux.HandleFunc("/_/portals", portalsHandler()) - - // REST - mux.HandleFunc("/api/portals", portalsRestHandler()) - mux.HandleFunc("/api/pages", pagesRestHandler()) -} - -func getAllFooterPortals() []templ.Component { - var allFooterPortals = make([]templ.Component, len(config.C.Portals)) - for i, configPortal := range config.C.Portals { - allFooterPortals[i] = components.FooterPortal(configPortal.Link, configPortal.Emoji, configPortal.Title, configPortal.External) - } - return allFooterPortals -} diff --git a/internal/routes/version_handler.go b/internal/routes/version_handler.go deleted file mode 100644 index ef18608..0000000 --- a/internal/routes/version_handler.go +++ /dev/null @@ -1,24 +0,0 @@ -package routes - -import ( - "net/http" - "sync" - - "github.com/a-h/templ" - "github.com/kodehat/portkey/internal/build" - "github.com/kodehat/portkey/internal/components" - "github.com/kodehat/portkey/internal/config" - "github.com/kodehat/portkey/internal/utils" -) - -func versionHandler() http.HandlerFunc { - var ( - init sync.Once - allFooterPortals []templ.Component - ) - init.Do(func() { - allFooterPortals = getAllFooterPortals() - }) - - return templ.Handler(components.ContentLayout(utils.PageTitle("Version", config.C.Title), "Version", components.Version(build.B), allFooterPortals, config.C.FooterText)).ServeHTTP -} diff --git a/internal/server/health_handler.go b/internal/server/health_handler.go new file mode 100644 index 0000000..85b2273 --- /dev/null +++ b/internal/server/health_handler.go @@ -0,0 +1,10 @@ +package server + +import "net/http" + +func healthHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + } +} diff --git a/internal/server/home_handler.go b/internal/server/home_handler.go new file mode 100644 index 0000000..382ea6b --- /dev/null +++ b/internal/server/home_handler.go @@ -0,0 +1,21 @@ +package server + +import ( + "net/http" + + "github.com/a-h/templ" + "github.com/kodehat/portkey/internal/components" + "github.com/kodehat/portkey/internal/config" +) + +func homeHandler() http.HandlerFunc { + home := components.HomePage() + return func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + w.WriteHeader(http.StatusNotFound) + templ.Handler(components.ContentLayout("404 Not Found", config.C, components.NotFound())).ServeHTTP(w, r) + return + } + templ.Handler(components.HomeLayout("Home", config.C, home)).ServeHTTP(w, r) + } +} diff --git a/internal/routes/page_handler.go b/internal/server/page_handler.go similarity index 55% rename from internal/routes/page_handler.go rename to internal/server/page_handler.go index 2948e58..d32c03f 100644 --- a/internal/routes/page_handler.go +++ b/internal/server/page_handler.go @@ -1,13 +1,11 @@ -package routes +package server import ( "net/http" - "sync" "github.com/a-h/templ" "github.com/kodehat/portkey/internal/components" "github.com/kodehat/portkey/internal/config" - "github.com/kodehat/portkey/internal/utils" ) type pageHandlerInfo struct { @@ -16,19 +14,11 @@ type pageHandlerInfo struct { } func pageHandler() []pageHandlerInfo { - var ( - init sync.Once - allFooterPortals []templ.Component - ) - init.Do(func() { - allFooterPortals = getAllFooterPortals() - }) - var pageHandlerInfos = make([]pageHandlerInfo, len(config.C.Pages)) for i, page := range config.C.Pages { pageHandlerInfos[i] = pageHandlerInfo{ pagePath: page.Path, - handlerFunc: templ.Handler(components.ContentLayout(utils.PageTitle(page.Heading, config.C.Title), page.Heading, components.ContentPage(page.Content), allFooterPortals, config.C.FooterText)).ServeHTTP, + handlerFunc: templ.Handler(components.ContentLayout(page.Heading, config.C, components.ContentPage(page.Content))).ServeHTTP, } } return pageHandlerInfos diff --git a/internal/routes/portals_handler.go b/internal/server/portals_handler.go similarity index 57% rename from internal/routes/portals_handler.go rename to internal/server/portals_handler.go index cbd649c..3934104 100644 --- a/internal/routes/portals_handler.go +++ b/internal/server/portals_handler.go @@ -1,29 +1,28 @@ -package routes +package server import ( "net/http" "strings" - "github.com/a-h/templ" "github.com/kodehat/portkey/internal/components" "github.com/kodehat/portkey/internal/config" + "github.com/kodehat/portkey/internal/models" "github.com/kodehat/portkey/internal/utils" ) func portalsHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("search") - var allHomePortals = make([]templ.Component, 0) + var homePortals = make([]models.Portal, 0) for _, configPortal := range config.C.Portals { - portal := components.HomePortal(configPortal.Link, configPortal.Emoji, configPortal.Title, configPortal.External) if query != "" { if strings.Contains(configPortal.Title, query) || utils.ArrSubStr(configPortal.Keywords, query) { - allHomePortals = append(allHomePortals, portal) + homePortals = append(homePortals, configPortal) } } else { - allHomePortals = append(allHomePortals, portal) + homePortals = append(homePortals, configPortal) } } - components.PortalPartial(allHomePortals).Render(r.Context(), w) + components.PortalPartial(homePortals).Render(r.Context(), w) } } diff --git a/internal/routes/rest_handler.go b/internal/server/rest_handler.go similarity index 97% rename from internal/routes/rest_handler.go rename to internal/server/rest_handler.go index 699cb7f..a455fd2 100644 --- a/internal/routes/rest_handler.go +++ b/internal/server/rest_handler.go @@ -1,4 +1,4 @@ -package routes +package server import ( "encoding/json" diff --git a/internal/server/routes.go b/internal/server/routes.go new file mode 100644 index 0000000..c7e771e --- /dev/null +++ b/internal/server/routes.go @@ -0,0 +1,31 @@ +package server + +import ( + "embed" + "log" + "net/http" +) + +func addRoutes(mux *http.ServeMux, logger *log.Logger, static embed.FS) { + // Home + mux.HandleFunc("/", homeHandler()) + + // Custom pages + for _, pageHandler := range pageHandler() { + mux.HandleFunc(pageHandler.pagePath, pageHandler.handlerFunc) + } + + // Fix pages + mux.HandleFunc("/version", versionHandler()) + mux.HandleFunc("/healthz", healthHandler()) + + // Static + mux.HandleFunc("/static/", staticHandler(static)) + + // htmx + mux.HandleFunc("/_/portals", portalsHandler()) + + // REST + mux.HandleFunc("/api/portals", portalsRestHandler()) + mux.HandleFunc("/api/pages", pagesRestHandler()) +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..580b6c7 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,20 @@ +package server + +import ( + "embed" + "log" + "net/http" + + "github.com/kodehat/portkey/internal/config" +) + +func NewServer( + logger *log.Logger, + config *config.Config, + static embed.FS, +) http.Handler { + mux := http.NewServeMux() + addRoutes(mux, logger, static) + var handler http.Handler = mux + return handler +} diff --git a/internal/routes/static_handler.go b/internal/server/static_handler.go similarity index 92% rename from internal/routes/static_handler.go rename to internal/server/static_handler.go index 72a739c..530f6fb 100644 --- a/internal/routes/static_handler.go +++ b/internal/server/static_handler.go @@ -1,4 +1,4 @@ -package routes +package server import ( "embed" diff --git a/internal/server/version_handler.go b/internal/server/version_handler.go new file mode 100644 index 0000000..5bacae5 --- /dev/null +++ b/internal/server/version_handler.go @@ -0,0 +1,14 @@ +package server + +import ( + "net/http" + + "github.com/a-h/templ" + "github.com/kodehat/portkey/internal/build" + "github.com/kodehat/portkey/internal/components" + "github.com/kodehat/portkey/internal/config" +) + +func versionHandler() http.HandlerFunc { + return templ.Handler(components.ContentLayout("Version", config.C, components.Version(build.B))).ServeHTTP +} diff --git a/internal/utils/http.go b/internal/utils/http.go deleted file mode 100644 index 9d29c10..0000000 --- a/internal/utils/http.go +++ /dev/null @@ -1,7 +0,0 @@ -package utils - -import "fmt" - -func BuildAddr(host string, port int) string { - return fmt.Sprintf("%s:%d", host, port) -} diff --git a/internal/utils/test.go b/internal/utils/test.go new file mode 100644 index 0000000..0644420 --- /dev/null +++ b/internal/utils/test.go @@ -0,0 +1,53 @@ +package utils + +import ( + "context" + "fmt" + "net/http" + "time" +) + +// WaitForReady calls the specified endpoint until it gets a 200 +// response or until the context is cancelled or the timeout is +// reached. +func WaitForReady( + ctx context.Context, + timeout time.Duration, + endpoint string, +) error { + client := http.Client{} + startTime := time.Now() + for { + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + endpoint, + nil, + ) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Error making request: %s\n", err.Error()) + continue + } + if resp.StatusCode == http.StatusOK { + resp.Body.Close() + return nil + } + resp.Body.Close() + + select { + case <-ctx.Done(): + return ctx.Err() + default: + if time.Since(startTime) >= timeout { + return fmt.Errorf("timeout reached while waiting for endpoint") + } + // wait a little while between checks + time.Sleep(250 * time.Millisecond) + } + } +} diff --git a/main.go b/main.go index 24b8f55..5654f50 100644 --- a/main.go +++ b/main.go @@ -6,12 +6,15 @@ import ( "fmt" "io" "log" + "net" "net/http" "os" + "os/signal" + "sync" + "time" "github.com/kodehat/portkey/internal/config" - "github.com/kodehat/portkey/internal/routes" - "github.com/kodehat/portkey/internal/utils" + "github.com/kodehat/portkey/internal/server" ) const LOGGER_PREFIX string = "[portkey] " @@ -21,17 +24,44 @@ var static embed.FS func main() { ctx := context.Background() + config.Load() if err := run(ctx, config.C, os.Stdin, os.Stdout, os.Stderr); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) os.Exit(1) } } -func run(ctx context.Context, c config.Config, stdin io.Reader, stdout, stderr io.Writer) error { +func run(ctx context.Context, config config.Config, stdin io.Reader, stdout, stderr io.Writer) error { + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) + defer cancel() logger := log.New(stdout, LOGGER_PREFIX, log.Lmsgprefix|log.Ldate|log.Ltime) - mux := http.NewServeMux() - routes.AddRoutes(mux, static) - addr := utils.BuildAddr(config.C.Host, config.C.Port) - logger.Printf("Listening on %s\n", addr) - return http.ListenAndServe(addr, mux) + srv := server.NewServer( + logger, + &config, + static, + ) + httpServer := &http.Server{ + Addr: net.JoinHostPort(config.Host, config.Port), + Handler: srv, + } + go func() { + logger.Printf("Listening on %s\n", httpServer.Addr) + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Fprintf(os.Stderr, "error listening and serving: %s\n", err) + } + }() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + <-ctx.Done() + // make a new context for the Shutdown (thanks Alessandro Rosetti) + shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + if err := httpServer.Shutdown(shutdownCtx); err != nil { + fmt.Fprintf(os.Stderr, "error shutting down http server: %s\n", err) + } + }() + wg.Wait() + return nil } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..dc7e5a9 --- /dev/null +++ b/main_test.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "os" + "testing" + "time" + + "github.com/kodehat/portkey/internal/config" + "github.com/kodehat/portkey/internal/models" + "github.com/kodehat/portkey/internal/utils" +) + +var CONFIG_EMPTY config.Config = config.Config{ + Host: "localhost", + Port: "3000", + Portals: []models.Portal{}, + Pages: []models.Page{}, +} + +func setup(t *testing.T, config config.Config) error { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + t.Cleanup(cancel) + go run(ctx, config, os.Stdin, os.Stdout, os.Stderr) + return utils.WaitForReady(ctx, time.Duration(5)*time.Second, "http://localhost:3000/healthz") +} + +func TestStart(t *testing.T) { + err := setup(t, CONFIG_EMPTY) + if err != nil { + t.Fatalf("server was unable to start %v", err) + } +}