diff --git a/cmd/polaris/audit.go b/cmd/polaris/audit.go
index 56c62cdf..5ab7b415 100644
--- a/cmd/polaris/audit.go
+++ b/cmd/polaris/audit.go
@@ -38,6 +38,7 @@ var auditOutputURL string
var auditOutputFile string
var auditOutputFormat string
var resourceToAudit string
+var useColor bool
func init() {
rootCmd.AddCommand(auditCmd)
@@ -47,7 +48,8 @@ func init() {
auditCmd.PersistentFlags().IntVar(&minScore, "set-exit-code-below-score", 0, "Set an exit code of 4 when the score is below this threshold (1-100).")
auditCmd.PersistentFlags().StringVar(&auditOutputURL, "output-url", "", "Destination URL to send audit results.")
auditCmd.PersistentFlags().StringVar(&auditOutputFile, "output-file", "", "Destination file for audit results.")
- auditCmd.PersistentFlags().StringVarP(&auditOutputFormat, "format", "f", "json", "Output format for results - json, yaml, or score.")
+ auditCmd.PersistentFlags().StringVarP(&auditOutputFormat, "format", "f", "json", "Output format for results - json, yaml, pretty, or score.")
+ auditCmd.PersistentFlags().BoolVar(&useColor, "color", true, "Whether to use color in pretty format.")
auditCmd.PersistentFlags().StringVar(&displayName, "display-name", "", "An optional identifier for the audit.")
auditCmd.PersistentFlags().StringVar(&resourceToAudit, "resource", "", "Audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend.")
}
@@ -61,7 +63,7 @@ var auditCmd = &cobra.Command{
config.DisplayName = displayName
}
- auditData := runAndReportAudit(cmd.Context(), config, auditPath, resourceToAudit, auditOutputFile, auditOutputURL, auditOutputFormat)
+ auditData := runAndReportAudit(cmd.Context(), config, auditPath, resourceToAudit, auditOutputFile, auditOutputURL, auditOutputFormat, useColor)
summary := auditData.GetSummary()
score := summary.GetScore()
@@ -75,7 +77,7 @@ var auditCmd = &cobra.Command{
},
}
-func runAndReportAudit(ctx context.Context, c conf.Configuration, auditPath, workload, outputFile, outputURL, outputFormat string) validator.AuditData {
+func runAndReportAudit(ctx context.Context, c conf.Configuration, auditPath, workload, outputFile, outputURL, outputFormat string, useColor bool) validator.AuditData {
// Create a kubernetes client resource provider
k, err := kube.CreateResourceProvider(ctx, auditPath, workload)
if err != nil {
@@ -98,6 +100,8 @@ func runAndReportAudit(ctx context.Context, c conf.Configuration, auditPath, wor
if err == nil {
outputBytes, err = yaml.JSONToYAML(jsonBytes)
}
+ } else if outputFormat == "pretty" {
+ outputBytes = []byte(auditData.GetPrettyOutput(useColor))
} else {
outputBytes, err = json.MarshalIndent(auditData, "", " ")
}
diff --git a/docs/.vuepress/config-extras.js b/docs/.vuepress/config-extras.js
index 81985d56..048ad3a5 100644
--- a/docs/.vuepress/config-extras.js
+++ b/docs/.vuepress/config-extras.js
@@ -33,6 +33,13 @@ module.exports = {
"/infrastructure-as-code",
],
},
+ {
+ title: "Usage",
+ collapsable: false,
+ children: [
+ "/cli",
+ ],
+ },
{
title: "Customization",
collapsable: false,
diff --git a/docs/README.md b/docs/README.md
index 66418d88..8d8ed8bc 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -3,7 +3,7 @@
Best Practices for Kubernetes Workload Configuration
-
+
@@ -11,11 +11,14 @@
+
+
+
Fairwinds' Polaris keeps your clusters sailing smoothly. It runs a variety of checks to ensure that
Kubernetes pods and controllers are configured using best practices, helping you avoid
-problems in the future. Polaris can be run in a few different modes:
+problems in the future.
Polaris can be run in three different modes:
* As a [dashboard](/dashboard), so you can audit what's running inside your cluster.
diff --git a/docs/cli.md b/docs/cli.md
new file mode 100644
index 00000000..90f07492
--- /dev/null
+++ b/docs/cli.md
@@ -0,0 +1,50 @@
+#### CLI Options
+
+```
+# top-level commands
+audit
+ Runs a one-time audit.
+dashboard
+ Runs the webserver for Polaris dashboard.
+help
+ Prints help, if you give it a command then it will print help for that command. Same as -h
+version
+ Prints the version of Polaris
+webhook
+ Runs the webhook webserver
+
+# global flags
+-c, --config string Location of Polaris configuration file.
+ --disallow-exemptions Disallow any exemptions from configuration file.
+ --kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster.
+ --log-level string Logrus log level. (default "info")
+
+# dashboard flags
+ --audit-path string If specified, audits one or more YAML files instead of a cluster.
+ --base-path string Path on which the dashboard is served. (default "/")
+ --display-name string An optional identifier for the audit.
+-h, --help help for dashboard
+ --listening-address string Listening Address for the dashboard webserver.
+ --load-audit-file string Runs the dashboard with data saved from a past audit.
+-p, --port int Port for the dashboard webserver. (default 8080)
+
+# audit flags
+ --audit-path string If specified, audits one or more YAML files instead of a cluster.
+ --color Whether to use color in pretty format. (default true)
+ --display-name string An optional identifier for the audit.
+-f, --format string Output format for results - json, yaml, pretty, or score. (default "json")
+-h, --help help for audit
+ --only-show-failed-tests If specified, audit output will only show failed tests.
+ --output-file string Destination file for audit results.
+ --output-url string Destination URL to send audit results.
+ --resource string Audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend.
+ --set-exit-code-below-score int Set an exit code of 4 when the score is below this threshold (1-100).
+ --set-exit-code-on-danger Set an exit code of 3 when the audit contains danger-level issues.
+
+
+# webhook flags
+ --disable-webhook-config-installer disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping.
+-h, --help help for webhook
+-p, --port int Port for the dashboard webserver. (default 9876)
+```
+
diff --git a/docs/cli/options.md b/docs/cli/options.md
deleted file mode 100644
index 14db18f6..00000000
--- a/docs/cli/options.md
+++ /dev/null
@@ -1,66 +0,0 @@
-#### CLI Options
-
-```
-# top-level commands
-audit
- Runs a one-time audit.
-dashboard
- Runs the webserver for Polaris dashboard.
-help
- Prints help, if you give it a command then it will print help for that command. Same as -h
-version
- Prints the version of Polaris
-webhook
- Runs the webhook webserver
-
-# high-level flags
--c, --config string
- Location of Polaris configuration file
---disallow-exemptions
- Disallow any exemptions from configuration file.
--h, --help
- Help for Polaris (same as help command)
---kubeconfig string
- Path to a kubeconfig. Only required if out-of-cluster.
---log-level string
- Logrus log level (default "info")
---master string
- The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.
-
-# dashboard flags
---audit-path string
- If specified, audits one or more YAML files instead of a cluster
---base-path string
- Path on which the dashboard is served (default "/")
---display-name string
- An optional identifier for the audit
---load-audit-file string
- Runs the dashboard with data saved from a past audit.
--p, --port int
- Port for the dashboard webserver (default 8080)
-
-# audit flags
---audit-path string
- If specified, audits one or more YAML files instead of a cluster
---resource string
- If specified, audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend
---display-name string
- An optional identifier for the audit
---format string
- Output format for results - json, yaml, or score (default "json")
---output-file string
- Destination file for audit results
---output-url string
- Destination URL to send audit results
---set-exit-code-below-score int
- Set an exit code of 4 when the score is below this threshold (1-100)
---set-exit-code-on-danger
- Set an exit code of 3 when the audit contains danger-level issues.
-
-# webhook flags
---disable-webhook-config-installer
- disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping
--p, --port int
- Port for the webhook webserver (default 9876)
-```
-
diff --git a/docs/infrastructure-as-code.md b/docs/infrastructure-as-code.md
index d2962e0a..b2ab842e 100644
--- a/docs/infrastructure-as-code.md
+++ b/docs/infrastructure-as-code.md
@@ -22,7 +22,7 @@ polaris version
## Running in a CI pipeline
-### Set minimum score for an exit code
+### Set minimum score for an exit code
You can tell the CLI to set an exit code if it detects certain issues with your YAML files.
For example, to fail if polaris detects *any* danger-level issues, or if the score drops below 90%:
```bash
@@ -31,6 +31,22 @@ polaris audit --audit-path ./deploy/ \
--set-exit-code-below-score 90
```
+### Pretty-print results
+By default, results are output as JSON. You can get human-readable output with
+the `--format=pretty` flag:
+
+```bash
+polaris audit --audit-path ./deploy/ \
+ --format=pretty
+```
+
+You can also disable colors and emoji:
+```bash
+polaris audit --audit-path ./deploy/ \
+ --format=pretty \
+ --color=false
+```
+
### Output only showing failed tests
The CLI to gives you ability to display results containing only failed tests.
For example:
diff --git a/go.mod b/go.mod
index 8f9ee664..3e5da904 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
cloud.google.com/go v0.74.0 // indirect
github.com/Azure/go-autorest/autorest v0.11.15 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.10 // indirect
+ github.com/fatih/color v1.7.0
github.com/gobuffalo/packr/v2 v2.8.1
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.1.3 // indirect
diff --git a/go.sum b/go.sum
index 8ecf5234..810cc8d1 100644
--- a/go.sum
+++ b/go.sum
@@ -166,6 +166,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@@ -427,8 +428,10 @@ github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY
github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
diff --git a/pkg/validator/output.go b/pkg/validator/output.go
index 32100238..85ddd8a5 100644
--- a/pkg/validator/output.go
+++ b/pkg/validator/output.go
@@ -15,8 +15,11 @@
package validator
import (
+ "fmt"
"time"
+ "github.com/fatih/color"
+
"github.com/fairwindsops/polaris/pkg/config"
)
@@ -25,6 +28,17 @@ const (
PolarisOutputVersion = "1.0"
)
+var (
+ successMessage = "🎉 Success"
+ dangerMessage = "❌ Danger"
+ warningMessage = "😬 Warning"
+)
+
+var (
+ titleColor = color.New(color.FgBlue).Add(color.Bold)
+ checkColor = color.New(color.FgCyan)
+)
+
// AuditData contains all the data from a full Polaris audit
type AuditData struct {
PolarisOutputVersion string
@@ -114,3 +128,74 @@ type ContainerResult struct {
func (res *ContainerResult) removeSuccessfulResults() {
res.Results.removeSuccessfulResults()
}
+
+func fillString(id string, l int) string {
+ for len(id) < l {
+ id += " "
+ }
+ return id
+}
+
+// GetPrettyOutput returns a human-readable string
+func (res AuditData) GetPrettyOutput(useColor bool) string {
+ color.NoColor = !useColor
+ str := titleColor.Sprint(fmt.Sprintf("\n\nPolaris audited %s %s at %s\n", res.SourceType, res.SourceName, res.AuditTime))
+ str += color.CyanString(fmt.Sprintf(" Nodes: %d | Namespaces: %d | Controllers: %d\n", res.ClusterInfo.Nodes, res.ClusterInfo.Namespaces, res.ClusterInfo.Controllers))
+ str += color.GreenString(fmt.Sprintf(" Final score: %d\n", res.Score))
+ str += "\n"
+ for _, result := range res.Results {
+ str += result.GetPrettyOutput() + "\n"
+ }
+ color.NoColor = false
+ return str
+}
+
+// GetPrettyOutput returns a human-readable string
+func (res Result) GetPrettyOutput() string {
+ str := titleColor.Sprint(fmt.Sprintf("%s %s in namespace %s\n", res.Kind, res.Name, res.Namespace))
+ str += res.Results.GetPrettyOutput()
+ if res.PodResult != nil {
+ str += res.PodResult.GetPrettyOutput()
+ }
+ return str
+}
+
+// GetPrettyOutput returns a human-readable string
+func (res PodResult) GetPrettyOutput() string {
+ str := res.Results.GetPrettyOutput()
+ for _, cont := range res.ContainerResults {
+ str += cont.GetPrettyOutput() + "\n"
+ }
+ return str
+}
+
+// GetPrettyOutput returns a human-readable string
+func (res ContainerResult) GetPrettyOutput() string {
+ str := titleColor.Sprint(fmt.Sprintf(" Container %s\n", res.Name))
+ str += res.Results.GetPrettyOutput()
+ return str
+}
+
+const minIDLength = 40
+
+// GetPrettyOutput returns a human-readable string
+func (res ResultSet) GetPrettyOutput() string {
+ indent := " "
+ str := ""
+ for _, msg := range res {
+ status := color.GreenString(successMessage)
+ if !msg.Success {
+ if msg.Severity == config.SeverityWarning {
+ status = color.YellowString(warningMessage)
+ } else {
+ status = color.RedString(dangerMessage)
+ }
+ }
+ if color.NoColor {
+ status = status[2:] // remove emoji
+ }
+ str += fmt.Sprintf("%s%s %s\n", indent, checkColor.Sprint(fillString(msg.ID, minIDLength-len(indent))), status)
+ str += fmt.Sprintf("%s %s - %s\n", indent, msg.Category, msg.Message)
+ }
+ return str
+}