diff --git a/main.go b/main.go index c773cadd..9573ad7e 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,9 @@ import ( "io/ioutil" "net/http" "os" + "strings" + "github.com/gorilla/mux" conf "github.com/reactiveops/fairwinds/pkg/config" "github.com/reactiveops/fairwinds/pkg/dashboard" "github.com/reactiveops/fairwinds/pkg/kube" @@ -84,18 +86,24 @@ func main() { } func startDashboardServer(c conf.Configuration, k *kube.ResourceProvider, port int) { - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + router := mux.NewRouter() + router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }) - http.HandleFunc("/results.json", func(w http.ResponseWriter, r *http.Request) { + router.HandleFunc("/results.json", func(w http.ResponseWriter, r *http.Request) { dashboard.EndpointHandler(w, r, c, k) }) - http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + router.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "public/favicon.ico") }) + router.HandleFunc("/details/{category}", func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + category := vars["category"] + category = strings.Replace(category, ".md", "", -1) + dashboard.DetailsHandler(w, r, category) + }) fileServer := http.FileServer(dashboard.GetAssetBox()) - http.Handle("/static/", http.StripPrefix("/static/", fileServer)) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return @@ -108,6 +116,8 @@ func startDashboardServer(c conf.Configuration, k *kube.ResourceProvider, port i } dashboard.MainHandler(w, r, auditData) }) + http.Handle("/static/", http.StripPrefix("/static/", fileServer)) + http.Handle("/", router) logrus.Infof("Starting Fairwinds dashboard server on port %d", port) logrus.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) diff --git a/pkg/dashboard/assets/css/check-details.css b/pkg/dashboard/assets/css/check-details.css new file mode 100644 index 00000000..84f14afe --- /dev/null +++ b/pkg/dashboard/assets/css/check-details.css @@ -0,0 +1,12 @@ +th, td { + padding: 4px 8px; + vertical-align: top; +} + +body { + font-family: "Helvetica", "Arial", sans-serif +} + +h1, h2, h3, h4, h5, h6 { + font-family: "Muli", "Helvetica", "Arial", sans-serif +} diff --git a/pkg/dashboard/assets/css/dashboard.css b/pkg/dashboard/assets/css/dashboard.css new file mode 100644 index 00000000..fee58230 --- /dev/null +++ b/pkg/dashboard/assets/css/dashboard.css @@ -0,0 +1,227 @@ +.charts { + height: 405px; +} + +.card.cluster { + margin-top: 15px; +} + +.card.namespace { + padding: 10px 20px; +} + +.card h3 { + margin: 0; + font-weight: 300; + font-size: 28px; + padding: 15px 20px 20px; +} + +.namespace h3 strong { + margin: 0; + font-weight: bold; +} + +.cluster-overview { + height: 204px; +} + +.cluster-overview .cluster-score { + width: 200px; + display: inline-block; + margin-left: 20px; +} + +.cluster-overview .cluster-score .weather { + font-size: 90px; + color: #444; + margin-bottom: 15px; +} + +.cluster-overview .cluster-score .sailing-message { + font-size: 16px; + line-height: 28px; + margin-left: 7px; + font-weight: 300; + color: #555; +} + +.cluster-overview .cluster-score .scores { + font-size: 16px; + margin-top: 10px; +} + +.cluster .expandable-table ul.message-list { + margin: 10px 26px; +} + +#clusterScoreChart { + width: 550px; + position: relative; + top: -227px; + left: 120px; +} + +.cluster-overview .result-messages { + margin: 0 20px; + display: inline-block; + position: relative; + left: -100px; + top: -45px; +} + +.cluster-overview .result-messages ul { + position: relative; + top: 0px; + left: 400px; + font-size: 20px; + line-height: 35px; +} + +.card.cluster .expandable-table { + margin-top: 20px; +} + +.card.cluster .detail-label { + display: inline-block; + min-width: 140px; + font-weight: bold; +} + +.expandable-table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; +} + +.expandable-table tr { + height: 50px; +} + +.expandable-table td { + padding: 15px 20px; + margin: 0; + font-size: 18px; + border-top: 1px solid #eee; +} + +.expandable-table .resource-info .name { + cursor: pointer; +} + +.expandable-table .resource-info .caret-expander { + display: inline-block; + width: 15px; + height: 15px; + margin-right: 10px; + background-image: url('../images/caret-right.svg'); + background-size: 13px auto; + background-repeat: no-repeat; + background-position: 2px center; +} + +.expandable-table .resource-info.expanded .caret-expander { + background-image: url('../images/caret-bottom.svg'); + background-position: center 2px; +} + +.expandable-table .resource-info .expandable-content { + display: none; +} + +.expandable-table .resource-info.expanded .expandable-content { + display: block; +} + +.namespace .resource-info .result-messages { + color: #6a6a6a; +} + +.namespace .result-messages h4 { + font-weight: bold; + font-size: 15px; + margin: 15px 25px 6px; +} + +ul.message-list { + list-style-type: none; + font-size: 13px; + line-height: 20px; + margin: 5px 35px; + padding: 0; + color: #6a6a6a; +} + +ul.message-list li { + margin-bottom: 5px; +} + +ul.message-list li i.message-icon { + display: inline-block; + margin-right: 7px; + text-align: center; + width: 20px; + font-size: 17px; + font-weight: bold; + position: relative; + bottom: -3px; +} + +.result-messages .success i.message-icon { + color: #8BD2DC; +} + +.result-messages .warning i.message-icon { + color: #f26c21; +} + +.result-messages .error i.message-icon { + color: #a11f4c; +} + +a.more-info { + color: #6a6a6a; + text-decoration: none; +} + +.namespace .status-bar { + vertical-align: top; + padding-top: 18px; +} +.namespace .status-bar .status { + float: right; + animation: fadeIn 2s; +} + +.cluster .status { + width: 280px; +} +.namespace .status { + width: 200px; +} + +.status div { + height: 15px; + border-radius: 10px; +} + +.status .passing { + background-color: #8BD2DC; + float: left; +} + +.status .warning { + background-color: #f26c21; + float: left; +} + +.status .failing { + background-color: #a11f4c; + width: 100%; +} + +@keyframes fadeIn { + 0% {opacity: 0;} + 100% {opacity: 1;} +} + diff --git a/pkg/dashboard/assets/css/main.css b/pkg/dashboard/assets/css/main.css index 70fe0eca..4ce90b85 100644 --- a/pkg/dashboard/assets/css/main.css +++ b/pkg/dashboard/assets/css/main.css @@ -2,50 +2,47 @@ body { margin: 0; font-family: 'Muli', 'Helvetica', 'Arial', sans-serif; background: #f5f5f5; + line-height: 1.3rem; } -.header { +.navbar { padding: 20px 20px 0; } -.header .header-content { +.navbar .navbar-content { margin: 0 auto; width: 900px; } -.header .header-right { +.navbar .navbar-right { padding: 15px; float: right; } -.header .logo { +.navbar .logo { width: 280px; } -.header span.oss-text { +.navbar span.oss-text { color: #23103A; display: block; font-size: 11px; margin: 3px 0; } -.header a { +.navbar a { text-decoration: none; } -.header .ro-logo { +.navbar .ro-logo { height: 40px; } -.dashboard-content { +.main-content { width: 960px; margin: 0 auto; } -.charts { - height: 405px; -} - .card { margin: 25px 20px; padding: 20px; @@ -55,225 +52,6 @@ body { border-radius: 5px; } -.card.cluster { - margin-top: 15px; -} - - -.card.namespace { - padding: 10px 20px; -} - -.card h3 { - margin: 0; - font-weight: 300; - font-size: 28px; - padding: 15px 20px 20px; -} - -.namespace h3 strong { - margin: 0; - font-weight: bold; -} - -.cluster-overview { - height: 204px; -} - -.cluster-overview .cluster-score { - width: 200px; - display: inline-block; - margin-left: 20px; -} - -.cluster-overview .cluster-score .weather { - font-size: 90px; - color: #444; - margin-bottom: 15px; -} - -.cluster-overview .cluster-score .sailing-message { - font-size: 16px; - line-height: 28px; - margin-left: 7px; - font-weight: 300; - color: #555; -} - -.cluster-overview .cluster-score .scores { - font-size: 16px; - margin-top: 10px; -} - -.cluster .expandable-table ul.message-list { - margin: 10px 26px; -} - -#clusterScoreChart { - width: 550px; - position: relative; - top: -242px; - left: 120px; -} - -.cluster-overview .result-messages { - margin: 0 20px; - display: inline-block; - position: relative; - left: -100px; - top: -45px; -} - -.cluster-overview .result-messages ul { - position: relative; - top: 0px; - left: 400px; - font-size: 20px; - line-height: 35px; -} - -.card.cluster .expandable-table { - margin-top: 20px; -} - -.card.cluster .detail-label { - display: inline-block; - min-width: 140px; - font-weight: bold; -} - -.expandable-table { - width: 100%; - border-spacing: 0; - border-collapse: collapse; -} - -.expandable-table tr { - height: 50px; -} - -.expandable-table td { - padding: 15px 20px; - margin: 0; - font-size: 18px; - border-top: 1px solid #eee; -} - -.expandable-table .resource-info .name { - cursor: pointer; -} - -.expandable-table .resource-info .caret-expander { - display: inline-block; - width: 15px; - height: 15px; - margin-right: 10px; - background-image: url('../images/caret-right.svg'); - background-size: 13px auto; - background-repeat: no-repeat; - background-position: 2px center; -} - -.expandable-table .resource-info.expanded .caret-expander { - background-image: url('../images/caret-bottom.svg'); - background-position: center 2px; -} - -.expandable-table .resource-info .expandable-content { - display: none; -} - -.expandable-table .resource-info.expanded .expandable-content { - display: block; -} - -.namespace .resource-info .result-messages { - color: #6a6a6a; -} - -.namespace .result-messages h4 { - font-weight: bold; - font-size: 15px; - margin: 15px 25px 6px; -} - -ul.message-list { - list-style-type: none; - font-size: 13px; - line-height: 20px; - margin: 5px 35px; - padding: 0; - color: #6a6a6a; -} - -ul.message-list li { - margin-bottom: 5px; -} - -ul.message-list li i { - display: inline-block; - margin-right: 7px; - text-align: center; - width: 20px; - font-size: 17px; - font-weight: bold; - position: relative; - bottom: -3px; -} - -.result-messages .success i { - color: #8BD2DC; -} - -.result-messages .warning i { - color: #f26c21; -} - -.result-messages .error i { - color: #a11f4c; -} - -.namespace .status-bar { - vertical-align: top; - padding-top: 18px; -} -.namespace .status-bar .status { - float: right; - animation: fadeIn 2s; -} - -.cluster .status { - width: 280px; -} -.namespace .status { - width: 200px; -} - -.status div { - height: 15px; - border-radius: 10px; -} - -.status .passing { - background-color: #8BD2DC; - float: left; -} - -.status .warning { - background-color: #f26c21; - float: left; -} - -.status .failing { - background-color: #a11f4c; - width: 100%; -} - -@keyframes fadeIn { - 0% {opacity: 0;} - 100% {opacity: 1;} -} - .footer { text-align: center; padding-top: 10px; @@ -288,3 +66,4 @@ ul.message-list li i { .footer a:hover { text-decoration: underline; } + diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go index 5be8d4b9..0d46f4f1 100644 --- a/pkg/dashboard/dashboard.go +++ b/pkg/dashboard/dashboard.go @@ -25,24 +25,30 @@ import ( "github.com/reactiveops/fairwinds/pkg/kube" "github.com/reactiveops/fairwinds/pkg/validator" "github.com/sirupsen/logrus" + "gitlab.com/golang-commonmark/markdown" ) const ( // MainTemplateName is the main template MainTemplateName = "main.gohtml" - // HeaderTemplateName contains the navbar - HeaderTemplateName = "header.gohtml" + // HeadTemplateName contains styles and meta info + HeadTemplateName = "head.gohtml" + // NavbarTemplateName contains the navbar + NavbarTemplateName = "navbar.gohtml" // PreambleTemplateName contains an empty preamble that can be overridden PreambleTemplateName = "preamble.gohtml" // DashboardTemplateName contains the content of the dashboard DashboardTemplateName = "dashboard.gohtml" // FooterTemplateName contains the footer FooterTemplateName = "footer.gohtml" + // CheckDetailsTemplateName is a page for rendering details about a given check + CheckDetailsTemplateName = "check-details.gohtml" ) var ( templateBox = (*packr.Box)(nil) assetBox = (*packr.Box)(nil) + markdownBox = (*packr.Box)(nil) ) // GetAssetBox returns a binary-friendly set of assets packaged from disk @@ -61,6 +67,14 @@ func GetTemplateBox() *packr.Box { return templateBox } +// GetMarkdownBox returns a binary-friendly set of markdown files with error details +func GetMarkdownBox() *packr.Box { + if markdownBox == (*packr.Box)(nil) { + markdownBox = packr.New("Markdown", "../../docs") + } + return markdownBox +} + // TemplateData is passed to the dashboard HTML template type TemplateData struct { AuditData validator.AuditData @@ -77,16 +91,22 @@ func GetBaseTemplate(name string) (*template.Template, error) { "getGrade": getGrade, "getScore": getScore, "getIcon": getIcon, + "getCategoryLink": getCategoryLink, }) - templateBox := GetTemplateBox() templateFileNames := []string{ DashboardTemplateName, - HeaderTemplateName, + HeadTemplateName, + NavbarTemplateName, PreambleTemplateName, FooterTemplateName, MainTemplateName, } + return parseTemplateFiles(tmpl, templateFileNames) +} + +func parseTemplateFiles(tmpl *template.Template, templateFileNames []string) (*template.Template, error) { + templateBox := GetTemplateBox() for _, fname := range templateFileNames { templateFile, err := templateBox.Find(fname) if err != nil { @@ -101,6 +121,16 @@ func GetBaseTemplate(name string) (*template.Template, error) { return tmpl, nil } +func writeTemplate(tmpl *template.Template, data *TemplateData, w http.ResponseWriter) { + buf := &bytes.Buffer{} + err := tmpl.Execute(buf, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + buf.WriteTo(w) +} + // MainHandler gets template data and renders the dashboard with it. func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.AuditData) { jsonData, err := json.Marshal(auditData) @@ -120,23 +150,14 @@ func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.Aud http.Error(w, "Error getting template data", 500) return } - - buf := &bytes.Buffer{} - err = tmpl.ExecuteTemplate(buf, "main", templateData) - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - buf.WriteTo(w) + writeTemplate(tmpl, &templateData, w) } // EndpointHandler gets template data and renders json with it. func EndpointHandler(w http.ResponseWriter, r *http.Request, c conf.Configuration, kubeResources *kube.ResourceProvider) { templateData, err := validator.RunAudit(c, kubeResources) if err != nil { - http.Error(w, "Error Fetching Deployments", 500) + http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError) return } @@ -144,3 +165,31 @@ func EndpointHandler(w http.ResponseWriter, r *http.Request, c conf.Configuratio w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(templateData) } + +// DetailsHandler returns details for a given error type +func DetailsHandler(w http.ResponseWriter, r *http.Request, category string) { + box := GetMarkdownBox() + contents, err := box.Find(category + ".md") + if err != nil { + http.Error(w, "Error details not found for category "+category, http.StatusNotFound) + return + } + md := markdown.New(markdown.XHTMLOutput(true)) + detailsHTML := "{{ define \"details\" }}" + md.RenderToString(contents) + "{{ end }}" + + templateFileNames := []string{ + HeadTemplateName, + NavbarTemplateName, + CheckDetailsTemplateName, + FooterTemplateName, + } + tmpl := template.New("check-details") + tmpl, err = parseTemplateFiles(tmpl, templateFileNames) + if err != nil { + logrus.Printf("Error getting template data %v", err) + http.Error(w, "Error getting template data", 500) + return + } + tmpl.Parse(detailsHTML) + writeTemplate(tmpl, nil, w) +} diff --git a/pkg/dashboard/helpers.go b/pkg/dashboard/helpers.go index fa37adbd..065bd8b6 100644 --- a/pkg/dashboard/helpers.go +++ b/pkg/dashboard/helpers.go @@ -16,6 +16,7 @@ package dashboard import ( "github.com/reactiveops/fairwinds/pkg/validator" + "strings" ) func getWarningWidth(counts validator.CountSummary, fullWidth int) uint { @@ -26,6 +27,10 @@ func getSuccessWidth(counts validator.CountSummary, fullWidth int) uint { return uint(float64(counts.Successes) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth)) } +func getCategoryLink(category string) string { + return strings.Replace(strings.ToLower(category), " ", "-", -1) +} + func getGrade(rs validator.ResultSummary) string { score := getScore(rs) if score >= 97 { diff --git a/pkg/dashboard/templates/check-details.gohtml b/pkg/dashboard/templates/check-details.gohtml new file mode 100644 index 00000000..15352169 --- /dev/null +++ b/pkg/dashboard/templates/check-details.gohtml @@ -0,0 +1,17 @@ + + + +
+ {{ template "head" . }} + + + + + {{ template "navbar" . }} +