mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-05-07 17:57:09 +00:00
This change follows up #635 and lets end-users decide to disallow exemption rules defined as part of the config file or the controller annotations (whether none, any or both). The main use case here is to be able to prevent users with edit privileges over a controller to add a new exemption rule through an annotation which may obfuscate the actual policies we want to enforce. Signed-off-by: Maxime VISONNEAU <maxime.visonneau@gmail.com> Co-authored-by: Robert Brennan <accounts@rbren.io>
273 lines
8.0 KiB
Go
273 lines
8.0 KiB
Go
// Copyright 2019 FairwindsOps Inc
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package dashboard
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/fairwindsops/polaris/pkg/config"
|
|
"github.com/fairwindsops/polaris/pkg/kube"
|
|
"github.com/fairwindsops/polaris/pkg/validator"
|
|
packr "github.com/gobuffalo/packr/v2"
|
|
"github.com/gorilla/mux"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
// MainTemplateName is the main template
|
|
MainTemplateName = "main.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"
|
|
)
|
|
|
|
var (
|
|
templateBox = (*packr.Box)(nil)
|
|
assetBox = (*packr.Box)(nil)
|
|
markdownBox = (*packr.Box)(nil)
|
|
)
|
|
|
|
// GetAssetBox returns a binary-friendly set of assets packaged from disk
|
|
func GetAssetBox() *packr.Box {
|
|
if assetBox == (*packr.Box)(nil) {
|
|
assetBox = packr.New("Assets", "assets")
|
|
}
|
|
return assetBox
|
|
}
|
|
|
|
// GetTemplateBox returns a binary-friendly set of templates for rendering the dash
|
|
func GetTemplateBox() *packr.Box {
|
|
if templateBox == (*packr.Box)(nil) {
|
|
templateBox = packr.New("Templates", "templates")
|
|
}
|
|
return templateBox
|
|
}
|
|
|
|
// templateData is passed to the dashboard HTML template
|
|
type templateData struct {
|
|
BasePath string
|
|
Config config.Configuration
|
|
AuditData validator.AuditData
|
|
FilteredAuditData validator.AuditData
|
|
JSON template.JS
|
|
}
|
|
|
|
// GetBaseTemplate puts together the dashboard template. Individual pieces can be overridden before rendering.
|
|
func GetBaseTemplate(name string) (*template.Template, error) {
|
|
tmpl := template.New(name).Funcs(template.FuncMap{
|
|
"getWarningWidth": getWarningWidth,
|
|
"getSuccessWidth": getSuccessWidth,
|
|
"getWeatherIcon": getWeatherIcon,
|
|
"getWeatherText": getWeatherText,
|
|
"getGrade": getGrade,
|
|
"getIcon": getIcon,
|
|
"getResultClass": getResultClass,
|
|
"getCategoryLink": getCategoryLink,
|
|
"getCategoryInfo": getCategoryInfo,
|
|
})
|
|
|
|
templateFileNames := []string{
|
|
DashboardTemplateName,
|
|
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 {
|
|
return nil, err
|
|
}
|
|
|
|
tmpl, err = tmpl.Parse(string(templateFile))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
|
|
func getConfigForQuery(base config.Configuration, query url.Values) config.Configuration {
|
|
c := base
|
|
switch query.Get("disallowExemptions") {
|
|
case "true":
|
|
c.DisallowExemptions = true
|
|
c.DisallowConfigExemptions = true
|
|
c.DisallowAnnotationExemptions = true
|
|
default:
|
|
c.DisallowExemptions = false
|
|
c.DisallowConfigExemptions = false
|
|
c.DisallowAnnotationExemptions = false
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
func stripUnselectedNamespaces(data *validator.AuditData, selectedNamespaces []string) {
|
|
newResults := []validator.Result{}
|
|
for _, res := range data.Results {
|
|
if stringInSlice(res.Namespace, selectedNamespaces) {
|
|
newResults = append(newResults, res)
|
|
}
|
|
}
|
|
data.Results = newResults
|
|
}
|
|
|
|
// GetRouter returns a mux router serving all routes necessary for the dashboard
|
|
func GetRouter(c config.Configuration, auditPath string, port int, basePath string, auditData *validator.AuditData) *mux.Router {
|
|
router := mux.NewRouter().PathPrefix(basePath).Subrouter()
|
|
fileServer := http.FileServer(GetAssetBox())
|
|
router.PathPrefix("/static/").Handler(http.StripPrefix(path.Join(basePath, "/static/"), fileServer))
|
|
|
|
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) {
|
|
adjustedConf := getConfigForQuery(c, r.URL.Query())
|
|
if auditData == nil {
|
|
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "", c)
|
|
if err != nil {
|
|
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
|
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var auditDataObj validator.AuditData
|
|
auditDataObj, err = validator.RunAudit(adjustedConf, k)
|
|
if err != nil {
|
|
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
auditData = &auditDataObj
|
|
}
|
|
|
|
JSONHandler(w, r, auditData)
|
|
})
|
|
|
|
router.HandleFunc("/details/{category}", func(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
category := vars["category"]
|
|
category = strings.Replace(category, ".md", "", -1)
|
|
})
|
|
|
|
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/" && r.URL.Path != basePath {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
adjustedConf := getConfigForQuery(c, r.URL.Query())
|
|
|
|
if auditData == nil {
|
|
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "", c)
|
|
if err != nil {
|
|
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
|
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var auditData validator.AuditData
|
|
auditData, err = validator.RunAudit(adjustedConf, k)
|
|
if err != nil {
|
|
logrus.Errorf("Error getting audit data: %v", err)
|
|
http.Error(w, "Error running audit", 500)
|
|
return
|
|
}
|
|
MainHandler(w, r, adjustedConf, auditData, basePath)
|
|
} else {
|
|
MainHandler(w, r, adjustedConf, *auditData, basePath)
|
|
}
|
|
|
|
})
|
|
return router
|
|
}
|
|
|
|
// MainHandler gets template data and renders the dashboard with it.
|
|
func MainHandler(w http.ResponseWriter, r *http.Request, c config.Configuration, auditData validator.AuditData, basePath string) {
|
|
jsonData, err := json.Marshal(auditData.GetSummary())
|
|
|
|
if err != nil {
|
|
http.Error(w, "Error serializing audit data", 500)
|
|
return
|
|
}
|
|
|
|
filteredAuditData := auditData
|
|
namespaces := r.URL.Query()["ns"]
|
|
if len(namespaces) > 0 {
|
|
stripUnselectedNamespaces(&filteredAuditData, namespaces)
|
|
}
|
|
|
|
data := templateData{
|
|
BasePath: basePath,
|
|
AuditData: auditData,
|
|
FilteredAuditData: filteredAuditData,
|
|
JSON: template.JS(jsonData),
|
|
Config: c,
|
|
}
|
|
tmpl, err := GetBaseTemplate("main")
|
|
if err != nil {
|
|
logrus.Printf("Error getting template data %v", err)
|
|
http.Error(w, "Error getting template data", 500)
|
|
return
|
|
}
|
|
writeTemplate(tmpl, &data, w)
|
|
}
|
|
|
|
// JSONHandler gets template data and renders json with it.
|
|
func JSONHandler(w http.ResponseWriter, r *http.Request, auditData *validator.AuditData) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(auditData)
|
|
}
|