diff --git a/README.md b/README.md index 21654259..d967617b 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,10 @@ Polaris validation checks fall into several different categories: * `config`: Specify a location for the Polaris config * `dashboard`: Runs the webserver for Polaris dashboard. -* `dashboard-port`: Port for the dashboard webserver (default 8080) +* `dashboard-port`: Port for the dashboard webserver (default `8080`) +* `dashboard-base-path`: Path on which the dashboard is being served (default `/`) * `webhook`: Runs the webhook webserver. -* `webhook-port`: Port for the webhook webserver (default 9876) +* `webhook-port`: Port for the webhook webserver (default `9876`) * `disable-webhook-config-installer`: disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping * `kubeconfig`: Paths to a kubeconfig. Only required if out-of-cluster. diff --git a/main.go b/main.go index d6df4eb4..23ffc22c 100644 --- a/main.go +++ b/main.go @@ -22,9 +22,7 @@ import ( "io/ioutil" "net/http" "os" - "strings" - "github.com/gorilla/mux" conf "github.com/reactiveops/polaris/pkg/config" "github.com/reactiveops/polaris/pkg/dashboard" "github.com/reactiveops/polaris/pkg/kube" @@ -53,6 +51,7 @@ func main() { audit := flag.Bool("audit", false, "Runs a one-time audit.") auditPath := flag.String("audit-path", "", "If specified, audits one or more YAML files instead of a cluster") dashboardPort := flag.Int("dashboard-port", 8080, "Port for the dashboard webserver") + dashboardBasePath := flag.String("dashboard-base-path", "/", "Path on which the dashboard is served") webhookPort := flag.Int("webhook-port", 9876, "Port for the webhook webserver") auditOutputURL := flag.String("output-url", "", "Destination URL to send audit results") auditOutputFile := flag.String("output-file", "", "Destination file for audit results") @@ -89,56 +88,17 @@ func main() { if *webhook { startWebhookServer(c, *disableWebhookConfigInstaller, *webhookPort) } else if *dashboard { - startDashboardServer(c, *auditPath, *dashboardPort) + startDashboardServer(c, *auditPath, *dashboardPort, *dashboardBasePath) } else if *audit { runAudit(c, *auditPath, *auditOutputFile, *auditOutputURL) } } -func startDashboardServer(c conf.Configuration, auditPath string, port int) { - router := mux.NewRouter() +func startDashboardServer(c conf.Configuration, auditPath string, port int, basePath string) { + router := dashboard.GetRouter(c, auditPath, port, basePath) router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }) - router.HandleFunc("/results.json", func(w http.ResponseWriter, r *http.Request) { - k, err := kube.CreateResourceProvider(auditPath) - if err != nil { - logrus.Errorf("Error fetching Kubernetes resources %v", err) - http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError) - return - } - dashboard.EndpointHandler(w, r, c, k) - }) - router.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, "pkg/dashboard/assets/favicon-32x32.png") - }) - 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()) - router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - k, err := kube.CreateResourceProvider(auditPath) - if err != nil { - logrus.Errorf("Error fetching Kubernetes resources %v", err) - http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError) - return - } - auditData, err := validator.RunAudit(c, k) - if err != nil { - logrus.Errorf("Error getting audit data: %v", err) - http.Error(w, "Error running audit", 500) - return - } - dashboard.MainHandler(w, r, auditData) - }) - http.Handle("/static/", http.StripPrefix("/static/", fileServer)) http.Handle("/", router) logrus.Infof("Starting Polaris dashboard server on port %d", port) diff --git a/pkg/dashboard/assets/css/Muli.css b/pkg/dashboard/assets/css/Muli.css index be0e42b6..76854c25 100644 --- a/pkg/dashboard/assets/css/Muli.css +++ b/pkg/dashboard/assets/css/Muli.css @@ -2,17 +2,17 @@ font-family: 'Muli'; font-style: normal; font-weight: 300; - src: local('Muli Light'), local('Muli-Light'), url(/static/webfonts/Muli-Light.tff) format('truetype'); + src: local('Muli Light'), local('Muli-Light'), url(../webfonts/Muli-Light.tff) format('truetype'); } @font-face { font-family: 'Muli'; font-style: normal; font-weight: 400; - src: local('Muli Regular'), local('Muli-Regular'), url(/static/webfonts/Muli-Regular.tff) format('truetype'); + src: local('Muli Regular'), local('Muli-Regular'), url(../webfonts/Muli-Regular.tff) format('truetype'); } @font-face { font-family: 'Muli'; font-style: normal; font-weight: 700; - src: local('Muli Bold'), local('Muli-Bold'), url(/static/webfonts/Muli-Bold.tff) format('truetype'); + src: local('Muli Bold'), local('Muli-Bold'), url(../webfonts/Muli-Bold.tff) format('truetype'); } diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go index 150715ad..e689a2c4 100644 --- a/pkg/dashboard/dashboard.go +++ b/pkg/dashboard/dashboard.go @@ -19,8 +19,10 @@ import ( "encoding/json" "html/template" "net/http" + "strings" packr "github.com/gobuffalo/packr/v2" + "github.com/gorilla/mux" conf "github.com/reactiveops/polaris/pkg/config" "github.com/reactiveops/polaris/pkg/kube" "github.com/reactiveops/polaris/pkg/validator" @@ -75,8 +77,9 @@ func GetMarkdownBox() *packr.Box { return markdownBox } -// TemplateData is passed to the dashboard HTML template -type TemplateData struct { +// templateData is passed to the dashboard HTML template +type templateData struct { + BasePath string AuditData validator.AuditData JSON template.JS } @@ -122,7 +125,7 @@ func parseTemplateFiles(tmpl *template.Template, templateFileNames []string) (*t return tmpl, nil } -func writeTemplate(tmpl *template.Template, data *TemplateData, w http.ResponseWriter) { +func writeTemplate(tmpl *template.Template, data *templateData, w http.ResponseWriter) { buf := &bytes.Buffer{} err := tmpl.Execute(buf, data) if err != nil { @@ -132,8 +135,62 @@ func writeTemplate(tmpl *template.Template, data *TemplateData, w http.ResponseW buf.WriteTo(w) } +// GetRouter returns a mux router serving all routes necessary for the dashboard +func GetRouter(c conf.Configuration, auditPath string, port int, basePath string) *mux.Router { + router := mux.NewRouter() + router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK")) + }) + router.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + favicon, err := GetAssetBox().Find("favicon-32x32.png") + if err != nil { + logrus.Errorf("Error getting favicon: %v", err) + http.Error(w, "Error getting favicon", http.StatusInternalServerError) + return + } + w.Write(favicon) + }) + router.HandleFunc("/results.json", func(w http.ResponseWriter, r *http.Request) { + k, err := kube.CreateResourceProvider(auditPath) + if err != nil { + logrus.Errorf("Error fetching Kubernetes resources %v", err) + http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError) + return + } + JSONHandler(w, r, c, k) + }) + router.HandleFunc("/details/{category}", func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + category := vars["category"] + category = strings.Replace(category, ".md", "", -1) + DetailsHandler(w, r, category, basePath) + }) + fileServer := http.FileServer(GetAssetBox()) + router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer)) + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + k, err := kube.CreateResourceProvider(auditPath) + if err != nil { + logrus.Errorf("Error fetching Kubernetes resources %v", err) + http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError) + return + } + auditData, err := validator.RunAudit(c, k) + if err != nil { + logrus.Errorf("Error getting audit data: %v", err) + http.Error(w, "Error running audit", 500) + return + } + MainHandler(w, r, auditData, basePath) + }) + return router +} + // MainHandler gets template data and renders the dashboard with it. -func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.AuditData) { +func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.AuditData, basePath string) { jsonData, err := json.Marshal(auditData) if err != nil { @@ -141,7 +198,8 @@ func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.Aud return } - templateData := TemplateData{ + data := templateData{ + BasePath: basePath, AuditData: auditData, JSON: template.JS(jsonData), } @@ -151,12 +209,12 @@ func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.Aud http.Error(w, "Error getting template data", 500) return } - writeTemplate(tmpl, &templateData, w) + writeTemplate(tmpl, &data, 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) +// JSONHandler gets template data and renders json with it. +func JSONHandler(w http.ResponseWriter, r *http.Request, c conf.Configuration, kubeResources *kube.ResourceProvider) { + auditData, err := validator.RunAudit(c, kubeResources) if err != nil { http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError) return @@ -164,11 +222,11 @@ func EndpointHandler(w http.ResponseWriter, r *http.Request, c conf.Configuratio w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(templateData) + json.NewEncoder(w).Encode(auditData) } // DetailsHandler returns details for a given error type -func DetailsHandler(w http.ResponseWriter, r *http.Request, category string) { +func DetailsHandler(w http.ResponseWriter, r *http.Request, category string, basePath string) { box := GetMarkdownBox() contents, err := box.Find(category + ".md") if err != nil { @@ -192,5 +250,8 @@ func DetailsHandler(w http.ResponseWriter, r *http.Request, category string) { return } tmpl.Parse(detailsHTML) - writeTemplate(tmpl, nil, w) + data := templateData{ + BasePath: basePath, + } + writeTemplate(tmpl, &data, w) } diff --git a/pkg/dashboard/templates/check-details.gohtml b/pkg/dashboard/templates/check-details.gohtml index 033aa721..32c6ce1b 100644 --- a/pkg/dashboard/templates/check-details.gohtml +++ b/pkg/dashboard/templates/check-details.gohtml @@ -3,7 +3,7 @@
{{ template "head" . }} - + diff --git a/pkg/dashboard/templates/dashboard.gohtml b/pkg/dashboard/templates/dashboard.gohtml index f695e54b..788a82fa 100644 --- a/pkg/dashboard/templates/dashboard.gohtml +++ b/pkg/dashboard/templates/dashboard.gohtml @@ -62,7 +62,7 @@