Repository: zegl/kube-score
Pull request #197 (2019-11-17 12:32:31)

ref: create renderer packages for human and CI render modes
This will enable the kube-score.com-project [1] to use the same output mode as the CLI.

1: https://github.com/kube-score/web


Patch:
diff --git a/cmd/kube-score/main.go b/cmd/kube-score/main.go
index d328c0b..939dac6 100644
--- a/cmd/kube-score/main.go
+++ b/cmd/kube-score/main.go
@@ -5,18 +5,17 @@ import (
 	"encoding/csv"
 	"encoding/json"
 	"fmt"
-	"github.com/eidolon/wordwrap"
-	"github.com/fatih/color"
-	flag "github.com/spf13/pflag"
-	"golang.org/x/crypto/ssh/terminal"
 	"io"
 	"io/ioutil"
 	"os"
-	"sort"
-	"strings"
+
+	flag "github.com/spf13/pflag"
+	"golang.org/x/crypto/ssh/terminal"
 
 	"github.com/zegl/kube-score/config"
 	"github.com/zegl/kube-score/parser"
+	"github.com/zegl/kube-score/renderer/ci"
+	"github.com/zegl/kube-score/renderer/human"
 	"github.com/zegl/kube-score/score"
 	"github.com/zegl/kube-score/scorecard"
 )
@@ -174,9 +173,9 @@ Use "-" as filename to read from STDIN.`)
 		if err != nil {
 			termWidth = 80
 		}
-		r = outputHuman(scoreCard, *verboseOutput, termWidth)
+		r = human.Human(scoreCard, *verboseOutput, termWidth)
 	} else {
-		r = outputCi(scoreCard)
+		r = ci.CI(scoreCard)
 	}
 
 	output, _ := ioutil.ReadAll(r)
@@ -209,173 +208,6 @@ func listChecks() {
 	output.Flush()
 }
 
-func safeRepeat(s string, count int) string {
-	if count < 0 {
-		return ""
-	}
-	return strings.Repeat(s, count)
-}
-
-func min(a, b int) int {
-	if a < b {
-		return a
-	}
-	return b
-}
-
-func outputHuman(scoreCard *scorecard.Scorecard, verboseOutput int, termWidth int) io.Reader {
-	// Print the items sorted by scorecard key
-	var keys []string
-	for k := range *scoreCard {
-		keys = append(keys, k)
-	}
-	sort.Strings(keys)
-
-	w := bytes.NewBufferString("")
-
-	for _, key := range keys {
-		scoredObject := (*scoreCard)[key]
-
-		// Headers for each object
-		var writtenHeaderChars int
-		writtenHeaderChars, _ = color.New(color.FgMagenta).Fprintf(w, "%s/%s %s", scoredObject.TypeMeta.APIVersion, scoredObject.TypeMeta.Kind, scoredObject.ObjectMeta.Name)
-		if scoredObject.ObjectMeta.Namespace != "" {
-			written2, _ := color.New(color.FgMagenta).Fprintf(w, " in %s", scoredObject.ObjectMeta.Namespace)
-			writtenHeaderChars += written2
-		}
-
-		// Adjust to termsize
-		fmt.Fprintf(w, safeRepeat(" ", min(80, termWidth)-writtenHeaderChars-2))
-
-		if scoredObject.AnyBelowOrEqualToGrade(scorecard.GradeCritical) {
-			fmt.Fprintf(w, "💥\n")
-		} else if scoredObject.AnyBelowOrEqualToGrade(scorecard.GradeWarning) {
-			fmt.Fprintf(w, "🤔\n")
-		} else {
-			fmt.Fprintf(w, "✅\n")
-		}
-
-		for _, card := range scoredObject.Checks {
-			r := outputHumanStep(card, verboseOutput, termWidth)
-			io.Copy(w, r)
-		}
-	}
-
-	return w
-}
-
-func outputHumanStep(card scorecard.TestScore, verboseOutput int, termWidth int) io.Reader {
-	w := bytes.NewBufferString("")
-
-	// Only print skipped items if verbosity is at least 2
-	if card.Skipped && verboseOutput < 2 {
-		return w
-	}
-
-	var col color.Attribute
-
-	if card.Skipped || card.Grade >= scorecard.GradeAllOK {
-		// Higher than or equal to --threshold-ok
-		col = color.FgGreen
-
-		// If verbose output is disabled, skip OK items in the output
-		if verboseOutput == 0 {
-			return w
-		}
-
-	} else if card.Grade >= scorecard.GradeWarning {
-		// Higher than or equal to --threshold-warning
-		col = color.FgYellow
-	} else {
-		// All lower than both --threshold-ok and --threshold-warning are critical
-		col = color.FgRed
-	}
-
-	if card.Skipped {
-		color.New(col).Fprintf(w, "    [SKIPPED] %s\n", card.Check.Name)
-	} else {
-		color.New(col).Fprintf(w, "    [%s] %s\n", card.Grade.String(), card.Check.Name)
-	}
-
-	for _, comment := range card.Comments {
-		fmt.Fprintf(w, "        · ")
-
-		if len(comment.Path) > 0 {
-			fmt.Fprintf(w, "%s -> ", comment.Path)
-		}
-
-		fmt.Fprint(w, comment.Summary)
-
-		if len(comment.Description) > 0 {
-			wrapWidth := termWidth - 12
-			if wrapWidth < 40 {
-				wrapWidth = 40
-			}
-			wrapper := wordwrap.Wrapper(wrapWidth, false)
-			wrapped := wrapper(comment.Description)
-			fmt.Fprintln(w)
-			fmt.Fprintf(w, wordwrap.Indent(wrapped, strings.Repeat(" ", 12), false))
-		}
-
-		fmt.Fprintln(w)
-	}
-
-	return w
-}
-
-// "Machine" / CI friendly output
-func outputCi(scoreCard *scorecard.Scorecard) io.Reader {
-	w := bytes.NewBufferString("")
-
-	// Print the items sorted by scorecard key
-	var keys []string
-	for k := range *scoreCard {
-		keys = append(keys, k)
-	}
-	sort.Strings(keys)
-
-	for _, key := range keys {
-		scoredObject := (*scoreCard)[key]
-
-		for _, card := range scoredObject.Checks {
-			if len(card.Comments) == 0 {
-				if card.Skipped {
-					fmt.Fprintf(w, "[SKIPPED] %s\n",
-						scoredObject.HumanFriendlyRef(),
-					)
-				} else {
-					fmt.Fprintf(w, "[%s] %s\n",
-						card.Grade.String(),
-						scoredObject.HumanFriendlyRef(),
-					)
-				}
-			}
-
-			for _, comment := range card.Comments {
-				message := comment.Summary
-				if comment.Path != "" {
-					message = "(" + comment.Path + ") " + comment.Summary
-				}
-
-				if card.Skipped {
-					fmt.Fprintf(w, "[SKIPPED] %s: %s\n",
-						scoredObject.HumanFriendlyRef(),
-						message,
-					)
-				} else {
-					fmt.Fprintf(w, "[%s] %s: %s\n",
-						card.Grade.String(),
-						scoredObject.HumanFriendlyRef(),
-						message,
-					)
-				}
-			}
-		}
-	}
-
-	return w
-}
-
 func listToStructMap(items *[]string) map[string]struct{} {
 	structMap := make(map[string]struct{})
 	for _, testID := range *items {
diff --git a/renderer/ci/ci.go b/renderer/ci/ci.go
new file mode 100644
index 0000000..0adbf59
--- /dev/null
+++ b/renderer/ci/ci.go
@@ -0,0 +1,65 @@
+// Package ci is currently considered to be in alpha status, and is not covered
+// by the API stability guarantees
+package ci
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"sort"
+
+	"github.com/zegl/kube-score/scorecard"
+)
+
+// "Machine" / CI friendly output
+func CI(scoreCard *scorecard.Scorecard) io.Reader {
+	w := bytes.NewBufferString("")
+
+	// Print the items sorted by scorecard key
+	var keys []string
+	for k := range *scoreCard {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+
+	for _, key := range keys {
+		scoredObject := (*scoreCard)[key]
+
+		for _, card := range scoredObject.Checks {
+			if len(card.Comments) == 0 {
+				if card.Skipped {
+					fmt.Fprintf(w, "[SKIPPED] %s\n",
+						scoredObject.HumanFriendlyRef(),
+					)
+				} else {
+					fmt.Fprintf(w, "[%s] %s\n",
+						card.Grade.String(),
+						scoredObject.HumanFriendlyRef(),
+					)
+				}
+			}
+
+			for _, comment := range card.Comments {
+				message := comment.Summary
+				if comment.Path != "" {
+					message = "(" + comment.Path + ") " + comment.Summary
+				}
+
+				if card.Skipped {
+					fmt.Fprintf(w, "[SKIPPED] %s: %s\n",
+						scoredObject.HumanFriendlyRef(),
+						message,
+					)
+				} else {
+					fmt.Fprintf(w, "[%s] %s: %s\n",
+						card.Grade.String(),
+						scoredObject.HumanFriendlyRef(),
+						message,
+					)
+				}
+			}
+		}
+	}
+
+	return w
+}
diff --git a/renderer/human/human.go b/renderer/human/human.go
new file mode 100644
index 0000000..c80fb96
--- /dev/null
+++ b/renderer/human/human.go
@@ -0,0 +1,129 @@
+// Package human is currently considered to be in alpha status, and is not covered
+// by the API stability guarantees
+package human
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"sort"
+	"strings"
+
+	"github.com/eidolon/wordwrap"
+	"github.com/fatih/color"
+	"github.com/zegl/kube-score/scorecard"
+)
+
+func Human(scoreCard *scorecard.Scorecard, verboseOutput int, termWidth int) io.Reader {
+	// Print the items sorted by scorecard key
+	var keys []string
+	for k := range *scoreCard {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+
+	w := bytes.NewBufferString("")
+
+	for _, key := range keys {
+		scoredObject := (*scoreCard)[key]
+
+		// Headers for each object
+		var writtenHeaderChars int
+		writtenHeaderChars, _ = color.New(color.FgMagenta).Fprintf(w, "%s/%s %s", scoredObject.TypeMeta.APIVersion, scoredObject.TypeMeta.Kind, scoredObject.ObjectMeta.Name)
+		if scoredObject.ObjectMeta.Namespace != "" {
+			written2, _ := color.New(color.FgMagenta).Fprintf(w, " in %s", scoredObject.ObjectMeta.Namespace)
+			writtenHeaderChars += written2
+		}
+
+		// Adjust to termsize
+		fmt.Fprintf(w, safeRepeat(" ", min(80, termWidth)-writtenHeaderChars-2))
+
+		if scoredObject.AnyBelowOrEqualToGrade(scorecard.GradeCritical) {
+			fmt.Fprintf(w, "💥\n")
+		} else if scoredObject.AnyBelowOrEqualToGrade(scorecard.GradeWarning) {
+			fmt.Fprintf(w, "🤔\n")
+		} else {
+			fmt.Fprintf(w, "✅\n")
+		}
+
+		for _, card := range scoredObject.Checks {
+			r := outputHumanStep(card, verboseOutput, termWidth)
+			io.Copy(w, r)
+		}
+	}
+
+	return w
+}
+
+func outputHumanStep(card scorecard.TestScore, verboseOutput int, termWidth int) io.Reader {
+	w := bytes.NewBufferString("")
+
+	// Only print skipped items if verbosity is at least 2
+	if card.Skipped && verboseOutput < 2 {
+		return w
+	}
+
+	var col color.Attribute
+
+	if card.Skipped || card.Grade >= scorecard.GradeAllOK {
+		// Higher than or equal to --threshold-ok
+		col = color.FgGreen
+
+		// If verbose output is disabled, skip OK items in the output
+		if verboseOutput == 0 {
+			return w
+		}
+
+	} else if card.Grade >= scorecard.GradeWarning {
+		// Higher than or equal to --threshold-warning
+		col = color.FgYellow
+	} else {
+		// All lower than both --threshold-ok and --threshold-warning are critical
+		col = color.FgRed
+	}
+
+	if card.Skipped {
+		color.New(col).Fprintf(w, "    [SKIPPED] %s\n", card.Check.Name)
+	} else {
+		color.New(col).Fprintf(w, "    [%s] %s\n", card.Grade.String(), card.Check.Name)
+	}
+
+	for _, comment := range card.Comments {
+		fmt.Fprintf(w, "        · ")
+
+		if len(comment.Path) > 0 {
+			fmt.Fprintf(w, "%s -> ", comment.Path)
+		}
+
+		fmt.Fprint(w, comment.Summary)
+
+		if len(comment.Description) > 0 {
+			wrapWidth := termWidth - 12
+			if wrapWidth < 40 {
+				wrapWidth = 40
+			}
+			wrapper := wordwrap.Wrapper(wrapWidth, false)
+			wrapped := wrapper(comment.Description)
+			fmt.Fprintln(w)
+			fmt.Fprintf(w, wordwrap.Indent(wrapped, strings.Repeat(" ", 12), false))
+		}
+
+		fmt.Fprintln(w)
+	}
+
+	return w
+}
+
+func safeRepeat(s string, count int) string {
+	if count < 0 {
+		return ""
+	}
+	return strings.Repeat(s, count)
+}
+
+func min(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}


Test patch:
diff --git a/renderer/ci/ci_test.go b/renderer/ci/ci_test.go
new file mode 100644
index 0000000..fcb49bc
--- /dev/null
+++ b/renderer/ci/ci_test.go
@@ -0,0 +1,111 @@
+package ci
+
+import (
+	"io/ioutil"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/zegl/kube-score/domain"
+	"github.com/zegl/kube-score/scorecard"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func getTestCard() *scorecard.Scorecard {
+	checks := []scorecard.TestScore{
+		{
+			Check: domain.Check{
+				Name: "test-warning-two-comments",
+			},
+			Grade: scorecard.GradeWarning,
+			Comments: []scorecard.TestScoreComment{
+				{
+					Path:        "a",
+					Summary:     "summary",
+					Description: "description",
+				},
+				{
+					// No path
+					Summary:     "summary",
+					Description: "description",
+				},
+			},
+		},
+		{
+			Check: domain.Check{
+				Name: "test-ok-comment",
+			},
+			Grade: scorecard.GradeAllOK,
+			Comments: []scorecard.TestScoreComment{
+				{
+					Path:        "a",
+					Summary:     "summary",
+					Description: "description",
+				},
+			},
+		},
+		{
+			Check: domain.Check{
+				Name: "test-skipped-comment",
+			},
+			Skipped: true,
+			Comments: []scorecard.TestScoreComment{
+				{
+					Path:        "a",
+					Summary:     "skipped sum",
+					Description: "skipped description",
+				},
+			},
+		},
+		{
+			Check: domain.Check{
+				Name: "test-skipped-no-comment",
+			},
+			Skipped: true,
+		},
+	}
+
+	return &scorecard.Scorecard{
+		"a": &scorecard.ScoredObject{
+			TypeMeta: v1.TypeMeta{
+				Kind:       "Testing",
+				APIVersion: "v1",
+			},
+			ObjectMeta: v1.ObjectMeta{
+				Name:      "foo",
+				Namespace: "foofoo",
+			},
+			Checks: checks,
+		},
+
+		// No namespace
+		"b": &scorecard.ScoredObject{
+			TypeMeta: v1.TypeMeta{
+				Kind:       "Testing",
+				APIVersion: "v1",
+			},
+			ObjectMeta: v1.ObjectMeta{
+				Name: "bar-no-namespace",
+			},
+			Checks: checks,
+		},
+	}
+}
+
+func TestCiOutput(t *testing.T) {
+	t.Parallel()
+	// Defaults
+	r := CI(getTestCard())
+	all, err := ioutil.ReadAll(r)
+	assert.Nil(t, err)
+	assert.Equal(t, `[WARNING] foo/foofoo v1/Testing: (a) summary
+[WARNING] foo/foofoo v1/Testing: summary
+[OK] foo/foofoo v1/Testing: (a) summary
+[SKIPPED] foo/foofoo v1/Testing: (a) skipped sum
+[SKIPPED] foo/foofoo v1/Testing
+[WARNING] bar-no-namespace v1/Testing: (a) summary
+[WARNING] bar-no-namespace v1/Testing: summary
+[OK] bar-no-namespace v1/Testing: (a) summary
+[SKIPPED] bar-no-namespace v1/Testing: (a) skipped sum
+[SKIPPED] bar-no-namespace v1/Testing
+`, string(all))
+}
diff --git a/cmd/kube-score/output_test.go b/renderer/human/output_test.go
similarity index 91%
rename from cmd/kube-score/output_test.go
rename to renderer/human/output_test.go
index a154e16..1141c9d 100644
--- a/cmd/kube-score/output_test.go
+++ b/renderer/human/output_test.go
@@ -1,4 +1,4 @@
-package main
+package human
 
 import (
 	"io/ioutil"
@@ -92,28 +92,9 @@ func getTestCard() *scorecard.Scorecard {
 	}
 }
 
-func TestCiOutput(t *testing.T) {
-	t.Parallel()
-	// Defaults
-	r := outputCi(getTestCard())
-	all, err := ioutil.ReadAll(r)
-	assert.Nil(t, err)
-	assert.Equal(t, `[WARNING] foo/foofoo v1/Testing: (a) summary
-[WARNING] foo/foofoo v1/Testing: summary
-[OK] foo/foofoo v1/Testing: (a) summary
-[SKIPPED] foo/foofoo v1/Testing: (a) skipped sum
-[SKIPPED] foo/foofoo v1/Testing
-[WARNING] bar-no-namespace v1/Testing: (a) summary
-[WARNING] bar-no-namespace v1/Testing: summary
-[OK] bar-no-namespace v1/Testing: (a) summary
-[SKIPPED] bar-no-namespace v1/Testing: (a) skipped sum
-[SKIPPED] bar-no-namespace v1/Testing
-`, string(all))
-}
-
 func TestHumanOutputDefault(t *testing.T) {
 	t.Parallel()
-	r := outputHuman(getTestCard(), 0, 100)
+	r := Human(getTestCard(), 0, 100)
 	all, err := ioutil.ReadAll(r)
 	assert.Nil(t, err)
 	assert.Equal(t, `v1/Testing foo in foofoo                                                      🤔
@@ -133,7 +114,7 @@ v1/Testing bar-no-namespace                                                   
 
 func TestHumanOutputVerbose1(t *testing.T) {
 	t.Parallel()
-	r := outputHuman(getTestCard(), 1, 100)
+	r := Human(getTestCard(), 1, 100)
 	all, err := ioutil.ReadAll(r)
 	assert.Nil(t, err)
 	assert.Equal(t, `v1/Testing foo in foofoo                                                      🤔
@@ -159,7 +140,7 @@ v1/Testing bar-no-namespace                                                   
 
 func TestHumanOutputVerbose2(t *testing.T) {
 	t.Parallel()
-	r := outputHuman(getTestCard(), 2, 100)
+	r := Human(getTestCard(), 2, 100)
 	all, err := ioutil.ReadAll(r)
 	assert.Nil(t, err)
 	assert.Equal(t, `v1/Testing foo in foofoo                                                      🤔
@@ -274,7 +255,7 @@ func getTestCardAllOK() *scorecard.Scorecard {
 
 func TestHumanOutputAllOKDefault(t *testing.T) {
 	t.Parallel()
-	r := outputHuman(getTestCardAllOK(), 0, 100)
+	r := Human(getTestCardAllOK(), 0, 100)
 	all, err := ioutil.ReadAll(r)
 	assert.Nil(t, err)
 	assert.Equal(t, `v1/Testing foo in foofoo                                                      ✅
@@ -316,7 +297,7 @@ func getTestCardLongDescription() *scorecard.Scorecard {
 
 func TestHumanOutputLogDescription120Width(t *testing.T) {
 	t.Parallel()
-	r := outputHuman(getTestCardLongDescription(), 0, 120)
+	r := Human(getTestCardLongDescription(), 0, 120)
 	all, err := ioutil.ReadAll(r)
 	assert.Nil(t, err)
 	assert.Equal(t, `v1/Testing foo in foofoo                                                      🤔
@@ -331,7 +312,7 @@ func TestHumanOutputLogDescription120Width(t *testing.T) {
 
 func TestHumanOutputLogDescription100Width(t *testing.T) {
 	t.Parallel()
-	r := outputHuman(getTestCardLongDescription(), 0, 100)
+	r := Human(getTestCardLongDescription(), 0, 100)
 	all, err := ioutil.ReadAll(r)
 	assert.Nil(t, err)
 	assert.Equal(t, `v1/Testing foo in foofoo                                                      🤔
@@ -347,7 +328,7 @@ func TestHumanOutputLogDescription100Width(t *testing.T) {
 
 func TestHumanOutputLogDescription80Width(t *testing.T) {
 	t.Parallel()
-	r := outputHuman(getTestCardLongDescription(), 0, 80)
+	r := Human(getTestCardLongDescription(), 0, 80)
 	all, err := ioutil.ReadAll(r)
 	assert.Nil(t, err)
 	assert.Equal(t, `v1/Testing foo in foofoo                                                      🤔
@@ -364,7 +345,7 @@ func TestHumanOutputLogDescription80Width(t *testing.T) {
 
 func TestHumanOutputLogDescription0Width(t *testing.T) {
 	t.Parallel()
-	r := outputHuman(getTestCardLongDescription(), 0, 0)
+	r := Human(getTestCardLongDescription(), 0, 0)
 	all, err := ioutil.ReadAll(r)
 	assert.Nil(t, err)
 	assert.Equal(t, `v1/Testing foo in foofoo🤔
@@ -417,7 +398,7 @@ func getTestCardLongTitle() *scorecard.Scorecard {
 
 func TestHumanOutputWithLongObjectNames(t *testing.T) {
 	t.Parallel()
-	r := outputHuman(getTestCardLongTitle(), 0, 80)
+	r := Human(getTestCardLongTitle(), 0, 80)
 	all, err := ioutil.ReadAll(r)
 	assert.Nil(t, err)
 	assert.Equal(t, `v1/Testing this-is-a-very-long-title-this-is-a-very-long-title-this-is-a-very-long-title-this-is-a-very-long-title-this-is-a-very-long-title in foofoo🤔