From 2ae88214188a2a03192511bcb0f9847c65954f0f Mon Sep 17 00:00:00 2001
From: Adam Reese <adam@reese.io>
Date: Mon, 21 Nov 2016 22:36:55 -0800
Subject: [PATCH] feat(kube): add schema validation

Adds validation against the swagger schema.

Example error:
Error: release telling-wildebeest failed: error validating "": error
validating data: expected type int, for field
spec.template.spec.containers[0].ports[0].containerPort, got string

Current error:
unable to decode "": [pos 177]: json: expect char '"' but got char 'n'"'
---
 pkg/kube/client.go      | 23 ++++++++++++++++++++++-
 pkg/kube/client_test.go | 40 ++++++++++++++++++++++++++++++++++------
 2 files changed, 56 insertions(+), 7 deletions(-)

diff --git a/pkg/kube/client.go b/pkg/kube/client.go
index ae66d95d8..d74a1d030 100644
--- a/pkg/kube/client.go
+++ b/pkg/kube/client.go
@@ -54,6 +54,10 @@ type Client struct {
 	// a client will still attempt to contact a live server. In these situations,
 	// this flag may need to be disabled.
 	IncludeThirdPartyAPIs bool
+	// Validate idicates whether to load a schema for validation.
+	Validate bool
+	// SchemaCacheDir is the path for loading cached schema.
+	SchemaCacheDir string
 }
 
 // New create a new Client
@@ -61,6 +65,8 @@ func New(config clientcmd.ClientConfig) *Client {
 	return &Client{
 		Factory:               cmdutil.NewFactory(config),
 		IncludeThirdPartyAPIs: true,
+		Validate:              true,
+		SchemaCacheDir:        clientcmd.RecommendedSchemaFile,
 	}
 }
 
@@ -97,8 +103,13 @@ func (c *Client) Create(namespace string, reader io.Reader) error {
 }
 
 func (c *Client) newBuilder(namespace string, reader io.Reader) *resource.Builder {
+	schema, err := c.Validator(c.Validate, c.SchemaCacheDir)
+	if err != nil {
+		log.Printf("warning: failed to load schema: %s", err)
+	}
 	return c.NewBuilder(c.IncludeThirdPartyAPIs).
 		ContinueOnError().
+		Schema(schema).
 		NamespaceParam(namespace).
 		DefaultNamespace().
 		Stream(reader, "").
@@ -280,7 +291,7 @@ func perform(c *Client, namespace string, reader io.Reader, fn ResourceActorFunc
 	infos, err := c.newBuilder(namespace, reader).Do().Infos()
 	switch {
 	case err != nil:
-		return err
+		return scrubValidationError(err)
 	case len(infos) == 0:
 		return ErrNoObjectsVisited
 	}
@@ -449,3 +460,13 @@ func findMatchingInfo(target *resource.Info, infos []*resource.Info) (*resource.
 	}
 	return nil, false
 }
+
+// scrubValidationError removes kubectl info from the message
+func scrubValidationError(err error) error {
+	const stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false"
+
+	if strings.Contains(err.Error(), stopValidateMessage) {
+		return goerrors.New(strings.Replace(err.Error(), "; "+stopValidateMessage, "", -1))
+	}
+	return err
+}
diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go
index 7cf5b8246..afd95a5a0 100644
--- a/pkg/kube/client_test.go
+++ b/pkg/kube/client_test.go
@@ -26,8 +26,10 @@ import (
 	"testing"
 
 	"k8s.io/kubernetes/pkg/api/meta"
+	"k8s.io/kubernetes/pkg/api/testapi"
 	"k8s.io/kubernetes/pkg/api/unversioned"
 	api "k8s.io/kubernetes/pkg/api/v1"
+	"k8s.io/kubernetes/pkg/api/validation"
 	"k8s.io/kubernetes/pkg/client/unversioned/fake"
 	"k8s.io/kubernetes/pkg/kubectl/resource"
 	"k8s.io/kubernetes/pkg/runtime"
@@ -67,12 +69,13 @@ func TestUpdateResource(t *testing.T) {
 
 func TestPerform(t *testing.T) {
 	tests := []struct {
-		name       string
-		namespace  string
-		reader     io.Reader
-		count      int
-		err        bool
-		errMessage string
+		name        string
+		namespace   string
+		reader      io.Reader
+		count       int
+		swaggerFile string
+		err         bool
+		errMessage  string
 	}{
 		{
 			name:      "Valid input",
@@ -85,6 +88,13 @@ func TestPerform(t *testing.T) {
 			reader:     strings.NewReader(""),
 			err:        true,
 			errMessage: "no objects visited",
+		}, {
+			name:        "Invalid schema",
+			namespace:   "test",
+			reader:      strings.NewReader(testInvalidServiceManifest),
+			swaggerFile: "../../vendor/k8s.io/kubernetes/api/swagger-spec/" + testapi.Default.GroupVersion().Version + ".json",
+			err:         true,
+			errMessage:  `error validating "": error validating data: expected type int, for field spec.ports[0].port, got string`,
 		},
 	}
 
@@ -105,6 +115,16 @@ func TestPerform(t *testing.T) {
 		c.ClientForMapping = func(mapping *meta.RESTMapping) (resource.RESTClient, error) {
 			return &fake.RESTClient{}, nil
 		}
+		c.Validator = func(validate bool, cacheDir string) (validation.Schema, error) {
+			if tt.swaggerFile == "" {
+				return validation.NullSchema{}, nil
+			}
+			data, err := ioutil.ReadFile(tt.swaggerFile)
+			if err != nil {
+				t.Fatalf("could not load swagger spec: %s", err)
+			}
+			return validation.NewSwaggerSchemaFromBytes(data, nil)
+		}
 
 		err := perform(c, tt.namespace, tt.reader, fn)
 		if (err != nil) != tt.err {
@@ -159,6 +179,14 @@ spec:
       targetPort: 9376
 `
 
+const testInvalidServiceManifest = `
+kind: Service
+apiVersion: v1
+spec:
+  ports:
+    - port: "80"
+`
+
 const testEndpointManifest = `
 kind: Endpoints
 apiVersion: v1
-- 
GitLab