Repository: zegl/kube-score (go)

Issue:
Should throw error on invalid label values
**Which version of kube-score are you using?**

> kube-score version: registry.gitlab.com/warmfusion/kube-score:0-7-0

**What did you do?**

Validated the following yaml with an invalid label value;
```yaml
apiVersion: v1
kind: Service
metadata:
  name: alertmanager
  labels:
    app.kubernetes.io/managed-by: "engineering/kustomize"
spec:
  type: ExternalName
  externalName: alertmanager.example.com
```

**What did you expect to see?**

A critical error indicating that the value of the label is not compatible with kubernetes.

**What did you see instead?**

No mention of any errors;

```
$ cat test.yaml
apiVersion: v1
kind: Service
metadata:
  name: alertmanager
  labels:
    app.kubernetes.io/managed-by: "engineering/kustomize"
spec:
  type: ExternalName
  externalName: alertmanager.example.com


$ docker run -it --rm -v $(pwd):/app registry.gitlab.com/warmfusion/kube-score:0-7-0 kube-score score /app/test.yaml
v1/Service alertmanager
    [OK] Stable version
    [OK] Service Targets Pod
    [OK] Service Type


$ docker run -it --rm -v $(pwd):/app registry.gitlab.com/warmfusion/kube-score:0-7-0 kube-score version
kube-score version: 0.7.0, commit: bc8f6ac008072bea3c33707f9d4a58265b1de30c, built: 2019-04-28T13:03:35Z
```

Pull request:
score: add a check that validates label values
This fixes #130

```
RELNOTE: Add check that validates label values
```

Patch:
diff --git a/Dockerfile b/Dockerfile
index 2cdae3b..0e31b46 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.12-stretch as builder
+FROM golang:1.11-stretch as builder
 WORKDIR /go/app
 ENV CGO_ENABLED=0
 
diff --git a/cmd/kube-score/main.go b/cmd/kube-score/main.go
index 8b19e52..3d25e06 100644
--- a/cmd/kube-score/main.go
+++ b/cmd/kube-score/main.go
@@ -1,16 +1,13 @@
 package main
 
 import (
-	"bytes"
 	"encoding/csv"
-	"encoding/json"
 	"fmt"
-	"github.com/fatih/color"
-	flag "github.com/spf13/pflag"
 	"io"
-	"io/ioutil"
 	"os"
-	"sort"
+
+	"github.com/fatih/color"
+	flag "github.com/spf13/pflag"
 
 	"github.com/zegl/kube-score/config"
 	"github.com/zegl/kube-score/parser"
@@ -82,7 +79,7 @@ func scoreFiles() error {
 	warningThreshold := fs.Int("threshold-warning", 5, "The score threshold for treating a score as WARNING. Grades below this threshold are CRITICAL. Must be between 1 and 10 (inclusive).")
 	verboseOutput := fs.Bool("v", false, "Verbose output")
 	printHelp := fs.Bool("help", false, "Print help")
-	outputFormat := fs.String("output-format", "human", "Set to 'human', 'json' or 'ci'. If set to ci, kube-score will output the program in a format that is easier to parse by other programs.")
+	outputFormat := fs.String("output-format", "human", "Set to 'human' or 'ci'. If set to ci, kube-score will output the program in a format that is easier to parse by other programs.")
 	ignoreTests := fs.StringSlice("ignore-test", []string{}, "Disable a test, can be set multiple times")
 	setDefault(fs, "score", false)
 
@@ -102,9 +99,9 @@ func scoreFiles() error {
 		return fmt.Errorf("Error: --threshold-ok and --threshold-warning must be set to a value between 1 and 10 inclusive.")
 	}
 
-	if *outputFormat != "human" && *outputFormat != "ci" && *outputFormat != "json" {
+	if *outputFormat != "human" && *outputFormat != "ci" {
 		fs.Usage()
-		return fmt.Errorf("Error: --output-format must be set to: 'human', 'json' or 'ci'")
+		return fmt.Errorf("Error: --output-format must be set to: 'human' or 'ci'")
 	}
 
 	filesToRead := fs.Args()
@@ -156,87 +153,21 @@ Use "-" as filename to read from STDIN.`)
 		ignoredTests[testID] = struct{}{}
 	}
 
-	var exitCode int
-	if scoreCard.AnyBelowOrEqualToGrade(scorecard.GradeCritical) {
-		exitCode = 1
-	} else if *exitOneOnWarning && scoreCard.AnyBelowOrEqualToGrade(scorecard.Grade(*warningThreshold)) {
-		exitCode = 1
-	} else {
-		exitCode = 0
-	}
-
-	var r io.Reader
-
-	if *outputFormat == "json" {
-		// TODO: Don't print tests that should be ignored, this is best solved by not executing those tests.
-		d, _ := json.MarshalIndent(scoreCard, "", "    ")
-		w := bytes.NewBufferString("")
-		w.WriteString(string(d))
-		r = w
-	} else if *outputFormat == "human" {
-		r = outputHuman(scoreCard, *okThreshold, *warningThreshold, ignoredTests)
-	} else {
-		r = outputCi(scoreCard, *okThreshold, *warningThreshold, ignoredTests)
-	}
-
-	output, _ := ioutil.ReadAll(r)
-	fmt.Print(string(output))
-	os.Exit(exitCode)
-	return nil
-}
+	hasWarning := false
+	hasCritical := false
 
-func listChecks() {
-	fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
-	printHelp := fs.Bool("help", false, "Print help")
-	setDefault(fs, "list", false)
-	fs.Parse(os.Args[2:])
-
-	if *printHelp {
-		fs.Usage()
-		return
-	}
-
-	allChecks := score.RegisterAllChecks(parser.Empty(), config.Configuration{})
-
-	output := csv.NewWriter(os.Stdout)
-	for _, c := range allChecks.All() {
-		output.Write([]string{c.ID, c.TargetType, c.Comment})
-	}
-	output.Flush()
-}
-
-func statusString(grade scorecard.Grade, okThreshold, warningThreshold int) string {
-	if grade >= scorecard.Grade(okThreshold) {
-		// Higher than or equal to --threshold-ok
-		return "OK"
-	} else if grade >= scorecard.Grade(warningThreshold) {
-		// Higher than or equal to --threshold-warning
-		return "WARNING"
-	} else {
-		// All lower than both --threshold-ok and --threshold-warning are critical
-		return "CRITICAL"
-	}
-}
-
-func outputHuman(scoreCard *scorecard.Scorecard, okThreshold, warningThreshold int, ignoredTests map[string]struct{}) 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]
+	// Detect which output format we should use
+	humanOutput := *outputFormat == "human"
 
+	for _, scoredObject := range *scoreCard {
 		// Headers for each object
-		color.New(color.FgMagenta).Fprintf(w, "%s/%s %s", scoredObject.TypeMeta.APIVersion, scoredObject.TypeMeta.Kind, scoredObject.ObjectMeta.Name)
-		if scoredObject.ObjectMeta.Namespace != "" {
-			color.New(color.FgMagenta).Fprintf(w, " in %s\n", scoredObject.ObjectMeta.Namespace)
-		} else {
-			fmt.Fprintln(w)
+		if humanOutput {
+			color.New(color.FgMagenta).Printf("%s/%s %s", scoredObject.TypeMeta.APIVersion, scoredObject.TypeMeta.Kind, scoredObject.ObjectMeta.Name)
+			if scoredObject.ObjectMeta.Namespace != "" {
+				color.New(color.FgMagenta).Printf(" in %s\n", scoredObject.ObjectMeta.Namespace)
+			} else {
+				fmt.Println()
+			}
 		}
 
 		for _, card := range scoredObject.Checks {
@@ -244,92 +175,89 @@ func outputHuman(scoreCard *scorecard.Scorecard, okThreshold, warningThreshold i
 				continue
 			}
 
-			r := outputHumanStep(card, okThreshold, warningThreshold)
-			io.Copy(w, r)
-		}
-
-	}
-
-	return w
-}
-
-func outputHumanStep(card scorecard.TestScore, okThreshold, warningThreshold int) io.Reader {
-	var col color.Attribute
+			var col color.Attribute
+			var status string
+
+			if card.Grade >= scorecard.Grade(*okThreshold) {
+				// Higher than or equal to --threshold-ok
+				col = color.FgGreen
+				status = "OK"
+			} else if card.Grade >= scorecard.Grade(*warningThreshold) {
+				// Higher than or equal to --threshold-warning
+				col = color.FgYellow
+				status = "WARNING"
+				hasWarning = true
+			} else {
+				// All lower than both --threshold-ok and --threshold-warning are critical
+				col = color.FgRed
+				status = "CRITICAL"
+				hasCritical = true
+			}
 
-	if card.Grade >= scorecard.Grade(okThreshold) {
-		// Higher than or equal to --threshold-ok
-		col = color.FgGreen
-	} else if card.Grade >= scorecard.Grade(warningThreshold) {
-		// 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 humanOutput {
+				color.New(col).Printf("    [%s] %s\n", status, card.Check.Name)
 
-	w := bytes.NewBufferString("")
+				for _, comment := range card.Comments {
+					fmt.Printf("        * ")
 
-	color.New(col).Fprintf(w, "    [%s] %s\n", statusString(card.Grade, okThreshold, warningThreshold), card.Check.Name)
+					if len(comment.Path) > 0 {
+						fmt.Printf("%s -> ", comment.Path)
+					}
 
-	for _, comment := range card.Comments {
-		fmt.Fprintf(w, "        * ")
+					fmt.Print(comment.Summary)
 
-		if len(comment.Path) > 0 {
-			fmt.Fprintf(w, "%s -> ", comment.Path)
-		}
+					if len(comment.Description) > 0 {
+						fmt.Printf("\n             %s", comment.Description)
+					}
 
-		fmt.Fprint(w, comment.Summary)
+					fmt.Println()
+				}
+			} else {
+				// "Machine" / CI friendly output
+				for _, comment := range card.Comments {
+					message := comment.Summary
+					if comment.Path != "" {
+						message = "(" + comment.Path + ") " + comment.Summary
+					}
+
+					fmt.Printf("[%s] %s: %s\n",
+						status,
+						scoredObject.HumanFriendlyRef(),
+						message,
+					)
+				}
 
-		if len(comment.Description) > 0 {
-			fmt.Fprintf(w, "\n             %s", comment.Description)
+			}
 		}
+	}
 
-		fmt.Fprintln(w)
+	if hasCritical {
+		os.Exit(1)
+	} else if hasWarning && *exitOneOnWarning {
+		os.Exit(1)
+	} else {
+		os.Exit(0)
 	}
 
-	return w
+	return nil
 }
 
-// "Machine" / CI friendly output
-func outputCi(scoreCard *scorecard.Scorecard, okThreshold, warningThreshold int, ignoredTests map[string]struct{}) io.Reader {
-	w := bytes.NewBufferString("")
+func listChecks() {
+	fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
+	printHelp := fs.Bool("help", false, "Print help")
+	setDefault(fs, "list", false)
+	fs.Parse(os.Args[2:])
 
-	// Print the items sorted by scorecard key
-	var keys []string
-	for k := range *scoreCard {
-		keys = append(keys, k)
+	if *printHelp {
+		fs.Usage()
+		return
 	}
-	sort.Strings(keys)
-
-	for _, key := range keys {
-		scoredObject := (*scoreCard)[key]
 
-		for _, card := range scoredObject.Checks {
-			if _, ok := ignoredTests[card.Check.ID]; ok {
-				continue
-			}
-
-			if len(card.Comments) == 0 {
-				fmt.Fprintf(w, "[%s] %s\n",
-					statusString(card.Grade, okThreshold, warningThreshold),
-					scoredObject.HumanFriendlyRef(),
-				)
-			}
-
-			for _, comment := range card.Comments {
-				message := comment.Summary
-				if comment.Path != "" {
-					message = "(" + comment.Path + ") " + comment.Summary
-				}
+	allChecks := score.RegisterAllChecks(parser.Empty(), config.Configuration{})
 
-				fmt.Fprintf(w, "[%s] %s: %s\n",
-					statusString(card.Grade, okThreshold, warningThreshold),
-					scoredObject.HumanFriendlyRef(),
-					message,
-				)
-			}
-		}
+	output := csv.NewWriter(os.Stdout)
+	for _, c := range allChecks.All() {
+		output.Write([]string{c.ID, c.TargetType, c.Comment})
 	}
-
-	return w
+	output.Flush()
 }
diff --git a/score/checks/checks.go b/score/checks/checks.go
index 1b2a05f..b0ac465 100644
--- a/score/checks/checks.go
+++ b/score/checks/checks.go
@@ -3,14 +3,15 @@ package checks
 import (
 	"strings"
 
-	ks "github.com/zegl/kube-score/domain"
-	"github.com/zegl/kube-score/scorecard"
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1beta1 "k8s.io/api/batch/v1beta1"
 	corev1 "k8s.io/api/core/v1"
 	extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
 	networkingv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	ks "github.com/zegl/kube-score/domain"
+	"github.com/zegl/kube-score/scorecard"
 )
 
 func New() *Checks {
@@ -44,7 +45,7 @@ func machineFriendlyName(in string) string {
 
 type MetaCheck struct {
 	ks.Check
-	Fn func(metav1.TypeMeta) scorecard.TestScore
+	Fn func(ks.BothMeta) scorecard.TestScore
 }
 
 type PodCheck struct {
@@ -94,7 +95,7 @@ type Checks struct {
 	cronjobs        map[string]CronJobCheck
 }
 
-func (c *Checks) RegisterMetaCheck(name, comment string, fn func(metav1.TypeMeta) scorecard.TestScore) {
+func (c *Checks) RegisterMetaCheck(name, comment string, fn func(meta ks.BothMeta) scorecard.TestScore) {
 	ch := NewCheck(name, "all", comment)
 	c.all = append(c.all, ch)
 	c.metas[machineFriendlyName(name)] = MetaCheck{ch, fn}
diff --git a/score/meta/labels.go b/score/meta/labels.go
new file mode 100644
index 0000000..a0270cb
--- /dev/null
+++ b/score/meta/labels.go
@@ -0,0 +1,25 @@
+package meta
+
+import (
+	"regexp"
+
+	"github.com/zegl/kube-score/domain"
+	"github.com/zegl/kube-score/score/checks"
+	"github.com/zegl/kube-score/scorecard"
+)
+
+func Register(allChecks *checks.Checks) {
+	allChecks.RegisterMetaCheck("Label values", "Validates label values", validateLabelValues)
+}
+
+func validateLabelValues(meta domain.BothMeta) (score scorecard.TestScore) {
+	score.Grade = scorecard.GradeAllOK
+	r := regexp.MustCompile("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$")
+	for key, value := range meta.ObjectMeta.Labels {
+		if !r.MatchString(value) {
+			score.Grade = scorecard.GradeCritical
+			score.AddComment(key, "Invalid label value", "The label value is invalid, and will not be accepted by Kubernetes")
+		}
+	}
+	return
+}
diff --git a/score/score.go b/score/score.go
index a068d24..b5d370c 100644
--- a/score/score.go
+++ b/score/score.go
@@ -9,6 +9,7 @@ import (
 	"github.com/zegl/kube-score/score/cronjob"
 	"github.com/zegl/kube-score/score/disruptionbudget"
 	"github.com/zegl/kube-score/score/ingress"
+	"github.com/zegl/kube-score/score/meta"
 	"github.com/zegl/kube-score/score/networkpolicy"
 	"github.com/zegl/kube-score/score/probes"
 	"github.com/zegl/kube-score/score/security"
@@ -32,6 +33,7 @@ func RegisterAllChecks(allObjects ks.AllTypes, cnf config.Configuration) *checks
 	service.Register(allChecks, allObjects, allObjects)
 	stable.Register(allChecks)
 	apps.Register(allChecks)
+	meta.Register(allChecks)
 
 	return allChecks
 }
@@ -52,7 +54,7 @@ func Score(allObjects ks.AllTypes, cnf config.Configuration) (*scorecard.Scoreca
 	for _, meta := range allObjects.Metas() {
 		o := scoreCard.NewObject(meta.TypeMeta, meta.ObjectMeta)
 		for _, test := range allChecks.Metas() {
-			o.Add(test.Fn(meta.TypeMeta), test.Check)
+			o.Add(test.Fn(meta), test.Check)
 		}
 	}
 
diff --git a/score/stable/stable_version.go b/score/stable/stable_version.go
index d26d54b..61360cc 100644
--- a/score/stable/stable_version.go
+++ b/score/stable/stable_version.go
@@ -2,9 +2,10 @@ package stable
 
 import (
 	"fmt"
+
+	"github.com/zegl/kube-score/domain"
 	"github.com/zegl/kube-score/score/checks"
 	"github.com/zegl/kube-score/scorecard"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func Register(allChecks *checks.Checks) {
@@ -12,7 +13,7 @@ func Register(allChecks *checks.Checks) {
 }
 
 // ScoreMetaStableAvailable checks if the supplied TypeMeta is an unstable object type, that has a stable(r) replacement
-func metaStableAvailable(meta metav1.TypeMeta) (score scorecard.TestScore) {
+func metaStableAvailable(meta domain.BothMeta) (score scorecard.TestScore) {
 	withStable := map[string]map[string]string{
 		"extensions/v1beta1": {
 			"Deployment": "apps/v1",
@@ -29,11 +30,11 @@ func metaStableAvailable(meta metav1.TypeMeta) (score scorecard.TestScore) {
 		},
 	}
 
-	if inVersion, ok := withStable[meta.APIVersion]; ok {
-		if recommendedVersion, ok := inVersion[meta.Kind]; ok {
+	if inVersion, ok := withStable[meta.TypeMeta.APIVersion]; ok {
+		if recommendedVersion, ok := inVersion[meta.TypeMeta.Kind]; ok {
 			score.Grade = scorecard.GradeWarning
 			score.AddComment("",
-				fmt.Sprintf("The apiVersion and kind %s/%s is deprecated", meta.APIVersion, meta.Kind),
+				fmt.Sprintf("The apiVersion and kind %s/%s is deprecated", meta.TypeMeta.APIVersion, meta.TypeMeta.Kind),
 				fmt.Sprintf("It's recommended to use %s instead", recommendedVersion),
 			)
 			return
diff --git a/scorecard/scorecard.go b/scorecard/scorecard.go
index 4cf1cdd..5b3181d 100644
--- a/scorecard/scorecard.go
+++ b/scorecard/scorecard.go
@@ -35,17 +35,6 @@ func (s Scorecard) NewObject(typeMeta metav1.TypeMeta, objectMeta metav1.ObjectM
 	return o
 }
 
-func (s Scorecard) AnyBelowOrEqualToGrade(threshold Grade) bool {
-	for _, o := range s {
-		for _, s := range o.Checks {
-			if s.Grade <= threshold {
-				return true
-			}
-		}
-	}
-	return false
-}
-
 type ScoredObject struct {
 	TypeMeta   metav1.TypeMeta
 	ObjectMeta metav1.ObjectMeta


Test patch:
diff --git a/cmd/kube-score/output_test.go b/cmd/kube-score/output_test.go
deleted file mode 100644
index bb6c1ac..0000000
--- a/cmd/kube-score/output_test.go
+++ /dev/null
@@ -1,192 +0,0 @@
-package main
-
-import (
-	"github.com/stretchr/testify/assert"
-	"io/ioutil"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"testing"
-
-	"github.com/zegl/kube-score/domain"
-	"github.com/zegl/kube-score/scorecard"
-)
-
-func TestCiOutput(t *testing.T) {
-	card := &scorecard.Scorecard{
-		"a": &scorecard.ScoredObject{
-			TypeMeta: v1.TypeMeta{
-				Kind:       "Testing",
-				APIVersion: "v1",
-			},
-			ObjectMeta: v1.ObjectMeta{
-				Name:      "foo",
-				Namespace: "foofoo",
-			},
-			Checks: []scorecard.TestScore{
-				{
-					Check: domain.Check{
-						Name: "TestingA",
-					},
-					Grade: scorecard.Grade(9),
-					Comments: []scorecard.TestScoreComment{
-						{
-							Path:        "a",
-							Summary:     "summary",
-							Description: "description",
-						},
-						{
-							// No path
-							Summary:     "summary",
-							Description: "description",
-						},
-					},
-				},
-			},
-		},
-
-		// No namespace
-		"b": &scorecard.ScoredObject{
-			TypeMeta: v1.TypeMeta{
-				Kind:       "Testing",
-				APIVersion: "v1",
-			},
-			ObjectMeta: v1.ObjectMeta{
-				Name: "bar-no-namespace",
-			},
-			Checks: []scorecard.TestScore{
-				{
-					Check: domain.Check{
-						Name: "TestingA",
-					},
-					Grade: scorecard.Grade(9),
-				},
-			},
-		},
-	}
-
-	ignoredTests := make(map[string]struct{})
-
-	// Defaults
-	r := outputCi(card, 10, 5, ignoredTests)
-	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
-[WARNING] bar-no-namespace v1/Testing
-`, string(all))
-
-	// OK at 9 or higher
-	r = outputCi(card, 9, 5, ignoredTests)
-	all, err = ioutil.ReadAll(r)
-	assert.Nil(t, err)
-	assert.Equal(t, `[OK] foo/foofoo v1/Testing: (a) summary
-[OK] foo/foofoo v1/Testing: summary
-[OK] bar-no-namespace v1/Testing
-`, string(all))
-
-	// OK at 8 or higher
-	r = outputCi(card, 8, 5, ignoredTests)
-	all, err = ioutil.ReadAll(r)
-	assert.Nil(t, err)
-	assert.Equal(t, `[OK] foo/foofoo v1/Testing: (a) summary
-[OK] foo/foofoo v1/Testing: summary
-[OK] bar-no-namespace v1/Testing
-`, string(all))
-}
-
-func TestHumanOutput(t *testing.T) {
-	card := &scorecard.Scorecard{
-		"a": &scorecard.ScoredObject{
-			TypeMeta: v1.TypeMeta{
-				Kind:       "Testing",
-				APIVersion: "v1",
-			},
-			ObjectMeta: v1.ObjectMeta{
-				Name:      "foo",
-				Namespace: "foofoo",
-			},
-			Checks: []scorecard.TestScore{
-				{
-					Check: domain.Check{
-						Name: "TestingA",
-					},
-					Grade: scorecard.Grade(9),
-					Comments: []scorecard.TestScoreComment{
-						{
-							Path:        "a",
-							Summary:     "summary",
-							Description: "description",
-						},
-						{
-							// No path
-							Summary:     "summary",
-							Description: "description",
-						},
-					},
-				},
-			},
-		},
-
-		// No namespace
-		"b": &scorecard.ScoredObject{
-			TypeMeta: v1.TypeMeta{
-				Kind:       "Testing",
-				APIVersion: "v1",
-			},
-			ObjectMeta: v1.ObjectMeta{
-				Name: "bar-no-namespace",
-			},
-			Checks: []scorecard.TestScore{
-				{
-					Check: domain.Check{
-						Name: "TestingA",
-					},
-					Grade: scorecard.Grade(9),
-				},
-			},
-		},
-	}
-
-	ignoredTests := make(map[string]struct{})
-
-	// Defaults
-	r := outputHuman(card, 10, 5, ignoredTests)
-	all, err := ioutil.ReadAll(r)
-	assert.Nil(t, err)
-	assert.Equal(t, `v1/Testing foo in foofoo
-    [WARNING] TestingA
-        * a -> summary
-             description
-        * summary
-             description
-v1/Testing bar-no-namespace
-    [WARNING] TestingA
-`, string(all))
-
-	// OK at 9 or higher
-	r = outputHuman(card, 9, 5, ignoredTests)
-	all, err = ioutil.ReadAll(r)
-	assert.Nil(t, err)
-	assert.Equal(t, `v1/Testing foo in foofoo
-    [OK] TestingA
-        * a -> summary
-             description
-        * summary
-             description
-v1/Testing bar-no-namespace
-    [OK] TestingA
-`, string(all))
-
-	// OK at 8 or higher
-	r = outputHuman(card, 8, 5, ignoredTests)
-	all, err = ioutil.ReadAll(r)
-	assert.Nil(t, err)
-	assert.Equal(t, `v1/Testing foo in foofoo
-    [OK] TestingA
-        * a -> summary
-             description
-        * summary
-             description
-v1/Testing bar-no-namespace
-    [OK] TestingA
-`, string(all))
-}
diff --git a/score/meta/labels_test.go b/score/meta/labels_test.go
new file mode 100644
index 0000000..9b62b9f
--- /dev/null
+++ b/score/meta/labels_test.go
@@ -0,0 +1,37 @@
+package meta
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/zegl/kube-score/domain"
+	"github.com/zegl/kube-score/scorecard"
+)
+
+func TestInvalidLabel(t *testing.T) {
+	s := validateLabelValues(domain.BothMeta{
+		ObjectMeta: metav1.ObjectMeta{
+			Labels: map[string]string{
+				"foo": "engineering/kustomize", // label values can't contain slashes
+				"bar": "baribar",
+			},
+		},
+	})
+	assert.Equal(t, scorecard.GradeCritical, s.Grade)
+	assert.Len(t, s.Comments, 1)
+	assert.Equal(t, "foo", s.Comments[0].Path)
+	assert.Equal(t, "Invalid label value", s.Comments[0].Summary)
+	assert.Equal(t, "The label value is invalid, and will not be accepted by Kubernetes", s.Comments[0].Description)
+}
+func TestOKLabel(t *testing.T) {
+	s := validateLabelValues(domain.BothMeta{
+		ObjectMeta: metav1.ObjectMeta{
+			Labels: map[string]string{
+				"foo": "foo-bar",
+			},
+		},
+	})
+	assert.Equal(t, scorecard.GradeAllOK, s.Grade)
+}