Compare commits

..
9 Commits
Author SHA1 Message Date
Bobby Brennan eab320af38 version 0.1.5 (#135) 2019-06-05 12:17:40 -04:00
Bobby Brennan 2494dd097b optionalize base path for dashboard (#134) 2019-06-04 16:44:41 -04:00
Bobby Brennan 12e10f9111 skip resources that are empty strings (#131) 2019-06-04 16:28:58 -04:00
Bobby Brennan 139e6611b1 add Nick and Kim as codeowners (#133) 2019-06-04 11:57:47 -04:00
Bobby Brennan 804171b289 Add office hours to README 2019-05-29 14:06:51 -04:00
Bobby Brennan f061e0e012 Fix helm for webhook (#129)
* Log all validation attempts

* add app selector for helm deployments
2019-05-24 10:21:38 -04:00
Rob Scott d5b3b6b4ae Merge pull request #126 from reactiveops/rs/init-except
Adding exception for init container resource checks
2019-05-23 16:57:50 +02:00
Rob Scott 9a03f87c0b adding exception for init container resource checks 2019-05-23 16:50:37 +02:00
Bobby Brennan 69d8b959d4 add release instructions (#122) 2019-05-20 16:45:48 -04:00
20 changed files with 164 additions and 93 deletions
+4
View File
@@ -39,3 +39,7 @@
* [Fix](https://github.com/reactiveops/polaris/issues/116): details pages getting template errors
* [Fix](https://github.com/reactiveops/polaris/issues/114): support all auth providers
* [Fix](https://github.com/reactiveops/polaris/issues/112): Ignore readiness probe for initContainers
# 0.1.5
* [Fix](https://github.com/reactiveops/polaris/issues/125): ignore limits/requests for initContainers
* [Fix](https://github.com/reactiveops/polaris/issues/132): support custom base path
+1 -1
View File
@@ -1 +1 @@
* @bobby-brennan @robscott
* @bobby-brennan @endzyme @kimschles @robscott
+16
View File
@@ -42,3 +42,19 @@ Each new pull request should:
- Contain a clear indication of if they're ready for review or a work in progress
- Be up to date and/or rebased on the master branch
## Creating a new release
* Change the version number everywhere. E.g. for `0.1.4` to `0.1.5`:
```
find ./ -type f ! -path "./vendor/*" ! -path "./.git/*" ! -name CHANGELOG.md -exec sed -i 's/0\.1\.4/0.1.5/g' {} \;
```
* Add relevant entries to CHANGELOG.md
* Create a PR to merge your changes
* Once the PR is merged, wait for CircleCI to finish building the merge commit
* Tag and push the latest:
```
git checkout master
git pull
git tag $VERSION
git push --tags
```
* CircleCI will add an entry to the releases page on GitHub
+6 -3
View File
@@ -5,7 +5,7 @@
[![Version][version-image]][version-link] [![CircleCI][circleci-image]][circleci-link] [![Go Report Card][goreport-image]][goreport-link]
</div>
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.1.4&color=239922
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.1.5&color=239922
[version-link]: https://github.com/reactiveops/polaris
[goreport-image]: https://goreportcard.com/badge/github.com/reactiveops/polaris
@@ -19,6 +19,8 @@ Polaris helps keep your cluster healthy. It runs a variety of checks to ensure t
- A dashboard that provides an overview of how well current deployments are configured within a cluster.
- An experimental validating webhook that can prevent any future deployments that do not live up to a configured standard.
**Want to learn more?** ReactiveOps holds [office hours on Zoom](https://zoom.us/j/951540265) the first Friday of every month, at 12pm Eastern. You can also reach out via email at `opensource@reactiveops.com`
## Dashboard
The Polaris dashboard is a way to get a simple visual overview of the current state of your Kubernetes deployments as well as a roadmap for what can be improved. The dashboard provides a cluster wide overview as well as breaking out results by category, namespace, and deployment.
@@ -106,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.
+1 -1
View File
@@ -160,7 +160,7 @@ spec:
- --dashboard
- --config
- /opt/app/config.yaml
image: 'quay.io/reactiveops/polaris:0.1.4'
image: 'quay.io/reactiveops/polaris:0.1.5'
imagePullPolicy: 'Always'
name: dashboard
ports:
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v1
description: Validation of best practices in your Kubernetes clusters
name: polaris
version: 0.1.4
version: 0.1.5
+2 -4
View File
@@ -35,9 +35,8 @@ Create chart name and version as used by the chart label.
Standard labels
*/}}
{{- define "polaris.labels" -}}
{{- if .Values.templateOnly -}}
app: {{ include "polaris.name" . }}
{{- else -}}
{{- if not .Values.templateOnly }}
app.kubernetes.io/name: {{ include "polaris.name" . }}
helm.sh/chart: {{ include "polaris.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
@@ -49,9 +48,8 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
Standard selector
*/}}
{{- define "polaris.selectors" -}}
{{- if .Values.templateOnly -}}
app: {{ include "polaris.name" . }}
{{- else -}}
{{- if not .Values.templateOnly }}
app.kubernetes.io/name: {{ include "polaris.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
+2 -2
View File
@@ -50,7 +50,7 @@ dashboard:
type: ClusterIP
image:
repository: quay.io/reactiveops/polaris
tag: 0.1.4
tag: 0.1.5
pullPolicy: Always
webhook:
@@ -58,7 +58,7 @@ webhook:
replicas: 1
image:
repository: quay.io/reactiveops/polaris
tag: 0.1.4
tag: 0.1.5
pullPolicy: Always
rbac:
+1 -1
View File
@@ -210,7 +210,7 @@ spec:
- --webhook
- --config
- /opt/app/config.yaml
image: 'quay.io/reactiveops/polaris:0.1.4'
image: 'quay.io/reactiveops/polaris:0.1.5'
imagePullPolicy: 'Always'
ports:
- containerPort: 9876
+5 -45
View File
@@ -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"
@@ -44,7 +42,7 @@ import (
const (
// Version represents the current release version of Polaris
Version = "0.1.4"
Version = "0.1.5"
)
func main() {
@@ -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)
+3 -3
View File
@@ -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');
}
+73 -12
View File
@@ -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)
}
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
{{ template "head" . }}
<link rel="stylesheet" href="/static/css/check-details.css">
<link rel="stylesheet" href="static/css/check-details.css">
</head>
<body>
+4 -4
View File
@@ -62,7 +62,7 @@
</div>
<div class="name"><span class="caret-expander"></span>{{ $category }}<span class="category-score">Score: <strong>{{ getScore $summary }}%</strong></span></div>
<div class="result-messages expandable-content">
<p class="category-info">{{ getCategoryInfo $category }} Refer to the <a href="/details/{{ getCategoryLink $category }}">Polaris documentation about {{ $category }}</a> for more information.</p>
<p class="category-info">{{ getCategoryInfo $category }} Refer to the <a href="details/{{ getCategoryLink $category }}">Polaris documentation about {{ $category }}</a> for more information.</p>
</div>
</div>
{{ end }} {{/* end range categories */}}
@@ -93,7 +93,7 @@
<li class="{{ .Type }}">
<i class="message-icon {{ getIcon $message }}"></i>
<span class="message">{{ .Message }}</span>
<a class="more-info" href="/details/{{ getCategoryLink .Category }}">
<a class="more-info" href="details/{{ getCategoryLink .Category }}">
<i class="far fa-question-circle"></i>
</a>
</li>
@@ -108,7 +108,7 @@
<li class="{{ .Type }}">
<i class="message-icon {{ getIcon $message }}"></i>
<span class="message">{{ .Message }}</span>
<a class="more-info" href="/details/{{ getCategoryLink .Category }}">
<a class="more-info" href="details/{{ getCategoryLink .Category }}">
<i class="far fa-question-circle"></i>
</a>
</li>
@@ -121,6 +121,6 @@
</div>
</div>
{{ end }} {{/* end range .AuditData.NamespacedResults */}}
<script src="/static/js/charts.js">
<script src="static/js/charts.js">
</script>
{{end}}
+11 -10
View File
@@ -1,18 +1,19 @@
{{ define "head" }}
<base href="{{ .BasePath }}">
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>ReactiveOps Polaris</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/static/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/static/favicon-16x16.png" sizes="16x16" />
<link href="/static/css/Muli.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/fontawesome-5.7.2.css">
<link rel="stylesheet" href="/static/css/dashboard.css">
<script type="text/javascript" src="/static/js/Chart-2.7.2.min.js"></script>
<script type="text/javascript" src="/static/js/cash-4.1.2.min.js"></script>
<script type="text/javascript" src="/static/js/main.js"></script>
<link rel="icon" type="image/png" href="static/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="static/favicon-16x16.png" sizes="16x16" />
<link href="static/css/Muli.css" rel="stylesheet">
<link rel="stylesheet" href="static/css/normalize.css">
<link rel="stylesheet" href="static/css/main.css">
<link rel="stylesheet" href="static/css/fontawesome-5.7.2.css">
<link rel="stylesheet" href="static/css/dashboard.css">
<script type="text/javascript" src="static/js/Chart-2.7.2.min.js"></script>
<script type="text/javascript" src="static/js/cash-4.1.2.min.js"></script>
<script type="text/javascript" src="static/js/main.js"></script>
{{ end }}
+2 -2
View File
@@ -2,12 +2,12 @@
<div class="navbar">
<div class="navbar-content">
<a href="/">
<img class="logo" src="/static/images/polaris-logo.png" alt="Polaris" />
<img class="logo" src="static/images/polaris-logo.png" alt="Polaris" />
</a>
<div class="navbar-right">
<a href="https://reactiveops.com?source=polaris" target="_blank">
<span class="oss-text">An Open Source Project By</span>
<img class="ro-logo" src="/static/images/ro-logo.png" alt="ReactiveOps" />
<img class="ro-logo" src="static/images/ro-logo.png" alt="ReactiveOps" />
</a>
</div>
</div>
+3
View File
@@ -99,6 +99,9 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
}
specs := regexp.MustCompile("\n-+\n").Split(string(contents), -1)
for _, spec := range specs {
if strings.TrimSpace(spec) == "" {
continue
}
err = addYaml(spec)
if err != nil {
logrus.Errorf("Error parsing YAML %v", err)
+6
View File
@@ -55,6 +55,12 @@ func ValidateContainer(cnConf *conf.Configuration, container *corev1.Container,
}
func (cv *ContainerValidation) validateResources(resConf *conf.Resources) {
// Only validate resources for primary containers. Although it can
// be helpful to set these in certain cases, it usually isn't
if cv.IsInitContainer {
return
}
category := messages.CategoryResources
res := cv.Container.Resources
+21
View File
@@ -161,6 +161,27 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
testValidateResources(t, &container, &resourceConf1, &expectedErrors, &expectedWarnings)
}
func TestValidateResourcesInit(t *testing.T) {
cvEmpty := ContainerValidation{
Container: &corev1.Container{},
ResourceValidation: &ResourceValidation{},
}
cvInit := ContainerValidation{
Container: &corev1.Container{},
ResourceValidation: &ResourceValidation{},
IsInitContainer: true,
}
parsedConf, err := conf.Parse([]byte(resourceConf1))
assert.NoError(t, err, "Expected no error when parsing config")
cvEmpty.validateResources(&parsedConf.Resources)
assert.Len(t, cvEmpty.Errors, 4)
cvInit.validateResources(&parsedConf.Resources)
assert.Len(t, cvInit.Errors, 0)
}
func TestValidateResourcesFullyValid(t *testing.T) {
cpuRequest, err := resource.ParseQuantity("300m")
assert.NoError(t, err, "Error parsing quantity")
+1 -3
View File
@@ -109,11 +109,9 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
if podResult.Summary.Totals.Errors > 0 {
allowed = false
logrus.Infof("%d validation errors found when validating %s", podResult.Summary.Totals.Errors, podResult.Name)
reason = getFailureReason(podResult)
}
logrus.Infof("%d validation errors found when validating %s", podResult.Summary.Totals.Errors, podResult.Name)
return admission.ValidationResponse(allowed, reason)
}