From fbe97e165cc0712adfc586fbcb359b7d7d74762f Mon Sep 17 00:00:00 2001 From: Pavlos Ratis Date: Sun, 3 Nov 2019 19:28:33 +0100 Subject: [PATCH] create basic web site to serve postmortems --- go.mod | 3 + go.sum | 7 ++ templates/categories.html | 10 ++ templates/category.html | 10 ++ templates/index.html | 11 ++ templates/layout.html | 16 +++ templates/postmortem.html | 18 +++ tmp/healthz | 1 - tool/handlers.go | 235 ++++++++++++++++++++++++++++++++++++++ tool/main.go | 18 +++ 10 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 templates/categories.html create mode 100644 templates/category.html create mode 100644 templates/index.html create mode 100644 templates/layout.html create mode 100644 templates/postmortem.html delete mode 100644 tmp/healthz create mode 100644 tool/handlers.go diff --git a/go.mod b/go.mod index 3a38ec0..be1aab1 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,8 @@ go 1.13 require ( github.com/gernest/front v0.0.0-20181129160812-ed80ca338b88 github.com/google/uuid v1.1.1 + github.com/gorilla/mux v1.7.3 + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + gopkg.in/russross/blackfriday.v2 v2.0.0 gopkg.in/yaml.v2 v2.2.4 // indirect ) diff --git a/go.sum b/go.sum index c0a98a7..3fb8d62 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,13 @@ github.com/gernest/front v0.0.0-20181129160812-ed80ca338b88 h1:fqfzqvgJfq5Sw7VZy github.com/gernest/front v0.0.0-20181129160812-ed80ca338b88/go.mod h1:FwEMwQ5+xky8tbzDLj72k2RAqXnFByLNwxg+9UZDtqU= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +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/russross/blackfriday.v2 v2.0.0 h1:+FlnIV8DSQnT7NZ43hcVKcdJdzZoeCmJj4Ql8gq5keA= +gopkg.in/russross/blackfriday.v2 v2.0.0/go.mod h1:6sSBNz/GtOm/pJTuh5UmBK2ZHfmnxGbl2NZg1UliSOI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/templates/categories.html b/templates/categories.html new file mode 100644 index 0000000..500b61c --- /dev/null +++ b/templates/categories.html @@ -0,0 +1,10 @@ +{{define "title"}}Categories{{end}} + +{{define "body"}} +

Category

+ +{{end}} diff --git a/templates/category.html b/templates/category.html new file mode 100644 index 0000000..1f4d3a8 --- /dev/null +++ b/templates/category.html @@ -0,0 +1,10 @@ +{{define "title"}}Category: {{ .Category }}{{end}} + +{{define "body"}} +

Category: {{ .Category }}

+ +{{end}} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..3f430cf --- /dev/null +++ b/templates/index.html @@ -0,0 +1,11 @@ +{{define "title"}}Postmortems{{end}} + +{{define "body"}} + +

Postmortems Index Page

+ +{{end}} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..b551f00 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,16 @@ +{{define "layout"}} + + + + + {{template "title" .}} + + +
+ Home + Categories +
+ {{ template "body" . }} + + +{{end}} diff --git a/templates/postmortem.html b/templates/postmortem.html new file mode 100644 index 0000000..7416f17 --- /dev/null +++ b/templates/postmortem.html @@ -0,0 +1,18 @@ +{{define "title"}}Postmortem{{end}} + +{{define "body"}} +

{{ .Company }}

+

Category

+ + + +

+{{ .Description }} +

+ +Source, Edit Metadata +{{end}} diff --git a/tmp/healthz b/tmp/healthz deleted file mode 100644 index 90b5016..0000000 --- a/tmp/healthz +++ /dev/null @@ -1 +0,0 @@ -ok. diff --git a/tool/handlers.go b/tool/handlers.go new file mode 100644 index 0000000..8efc159 --- /dev/null +++ b/tool/handlers.go @@ -0,0 +1,235 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "text/template" + + "github.com/gorilla/mux" + blackfriday "gopkg.in/russross/blackfriday.v2" +) + +// createHandlers sets the routes for the web server. +func createHandlers() *mux.Router { + router := mux.NewRouter() + router.HandleFunc("/", indexHandler).Methods("GET") + router.HandleFunc("/postmortem/{id}", postmortemPageHandler).Methods("GET") + router.HandleFunc("/categories", categoriesPageHandler).Methods("GET") + router.HandleFunc("/category/{category}", categoryPageHandler).Methods("GET") + router.HandleFunc("/healthz", healthzHandler).Methods("GET") + + return router +} + +// healthzHandler serves an availability check endpoint. +func healthzHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/plain") + + if _, err := w.Write([]byte("ok.")); err != nil { + log.Printf("Error writing response to healthz request") + } +} + +// loadPostmortem loads the postmortem data in memory. +func loadPostmortem(dir, filename string) (*Postmortem, error) { + f, err := os.Open(filepath.Join(dir, filename)) + if err != nil { + return nil, fmt.Errorf("error opening postmortem: %w", err) + } + + pm, err := Parse(f) + if err != nil { + return nil, fmt.Errorf("error parsing file %s: %w", f.Name(), err) + } + + return pm, nil +} + +// loadPostmortems parses the postmortem files +// and returns a slice with their content. +func loadPostmortems(dir string) ([]*Postmortem, error) { + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("error opening data folder: %w", err) + } + + var pms []*Postmortem + + for _, path := range files { + pm, err := loadPostmortem(dir, path.Name()) + if err != nil { + return nil, err + } + + pms = append(pms, pm) + } + + return pms, nil +} + +func categoriesPageHandler(w http.ResponseWriter, r *http.Request) { + lp := filepath.Join("templates", "layout.html") + fp := filepath.Join("templates", "categories.html") + + _, err := os.Stat(fp) + if err != nil { + if os.IsNotExist(err) { + // Template doesn't exist, return 404. + http.NotFound(w, r) + return + } + } + + tmpl, err := template.ParseFiles(lp, fp) + if err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + + return + } + + if err := tmpl.ExecuteTemplate(w, "layout", Categories); err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + } +} + +func getPosmortemByCategory(pms []*Postmortem, category string) []Postmortem { + var ctpm []Postmortem + + for _, pm := range pms { + for _, c := range pm.Categories { + if c == category { + ctpm = append(ctpm, *pm) + } + } + } + + return ctpm +} + +func categoryPageHandler(w http.ResponseWriter, r *http.Request) { + ct := mux.Vars(r)["category"] + + pms, err := loadPostmortems(*dir) + if err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + + return + } + + ctpm := getPosmortemByCategory(pms, ct) + + lp := filepath.Join("templates", "layout.html") + fp := filepath.Join("templates", "category.html") + + _, err = os.Stat(fp) + if err != nil { + if os.IsNotExist(err) { + // Template doesn't exist, return 404. + http.NotFound(w, r) + return + } + } + + tmpl, err := template.ParseFiles(lp, fp) + if err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + + return + } + + page := struct { + Category string + Postmortems []Postmortem + }{ + Category: ct, + Postmortems: ctpm, + } + + if err := tmpl.ExecuteTemplate(w, "layout", page); err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + } +} + +func postmortemPageHandler(w http.ResponseWriter, r *http.Request) { + pmID := mux.Vars(r)["id"] + + pm, err := loadPostmortem(*dir, pmID+".md") + if err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + } + + lp := filepath.Join("templates", "layout.html") + fp := filepath.Join("templates", "postmortem.html") + + _, err = os.Stat(fp) + if err != nil { + if os.IsNotExist(err) { + // Template doesn't exist, return 404. + http.NotFound(w, r) + return + } + } + + // Convert Markdown formatting of descriptions to HTML. + htmlDesc := blackfriday.Run([]byte(pm.Description)) + pm.Description = string(htmlDesc) + + tmpl, err := template.ParseFiles(lp, fp) + if err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + + return + } + + if err := tmpl.ExecuteTemplate(w, "layout", pm); err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + } +} + +func indexHandler(w http.ResponseWriter, r *http.Request) { + pms, err := loadPostmortems(*dir) + if err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + + return + } + + lp := filepath.Join("templates", "layout.html") + fp := filepath.Join("templates", "index.html") + + _, err = os.Stat(fp) + if err != nil { + if os.IsNotExist(err) { + // Template doesn't exist, return 404. + http.NotFound(w, r) + return + } + } + + tmpl, err := template.ParseFiles(lp, fp) + if err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + + return + } + + if err := tmpl.ExecuteTemplate(w, "layout", pms); err != nil { + log.Println(err.Error()) + http.Error(w, http.StatusText(500), http.StatusInternalServerError) + } +} diff --git a/tool/main.go b/tool/main.go index 0222904..cf88153 100644 --- a/tool/main.go +++ b/tool/main.go @@ -6,8 +6,10 @@ import ( "fmt" "io/ioutil" "log" + "net/http" "os" "path/filepath" + "time" ) var ( @@ -84,6 +86,19 @@ func Generate(d string) error { }) } +// Serve serves the content of the website. +func Serve() { + router := createHandlers() + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + fmt.Printf("%s Server listening on *:%s\n", time.Now().Format("2006-01-02 15:04:05"), port) + log.Fatal(http.ListenAndServe(":"+port, router)) +} + func main() { flag.Usage = usage flag.Parse() @@ -111,6 +126,8 @@ func main() { err = Generate(*dir) case "validate": err = ValidateDir(*dir) + case "serve": + Serve() default: log.Fatalf("%s is not a valid action", *action) } @@ -134,4 +151,5 @@ Actions: extract Extract postmortems from the collection and create separate files. generate Generate JSON files from the postmortem Markdown files. validate Validate the postmortem files in the directory. +serve Serve the postmortem files in a small website. `