Commit 2ae88214 authored by Adam Reese's avatar Adam Reese

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'"'
parent e77d564b
...@@ -54,6 +54,10 @@ type Client struct { ...@@ -54,6 +54,10 @@ type Client struct {
// a client will still attempt to contact a live server. In these situations, // a client will still attempt to contact a live server. In these situations,
// this flag may need to be disabled. // this flag may need to be disabled.
IncludeThirdPartyAPIs bool 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 // New create a new Client
...@@ -61,6 +65,8 @@ func New(config clientcmd.ClientConfig) *Client { ...@@ -61,6 +65,8 @@ func New(config clientcmd.ClientConfig) *Client {
return &Client{ return &Client{
Factory: cmdutil.NewFactory(config), Factory: cmdutil.NewFactory(config),
IncludeThirdPartyAPIs: true, IncludeThirdPartyAPIs: true,
Validate: true,
SchemaCacheDir: clientcmd.RecommendedSchemaFile,
} }
} }
...@@ -97,8 +103,13 @@ func (c *Client) Create(namespace string, reader io.Reader) error { ...@@ -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 { 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). return c.NewBuilder(c.IncludeThirdPartyAPIs).
ContinueOnError(). ContinueOnError().
Schema(schema).
NamespaceParam(namespace). NamespaceParam(namespace).
DefaultNamespace(). DefaultNamespace().
Stream(reader, ""). Stream(reader, "").
...@@ -280,7 +291,7 @@ func perform(c *Client, namespace string, reader io.Reader, fn ResourceActorFunc ...@@ -280,7 +291,7 @@ func perform(c *Client, namespace string, reader io.Reader, fn ResourceActorFunc
infos, err := c.newBuilder(namespace, reader).Do().Infos() infos, err := c.newBuilder(namespace, reader).Do().Infos()
switch { switch {
case err != nil: case err != nil:
return err return scrubValidationError(err)
case len(infos) == 0: case len(infos) == 0:
return ErrNoObjectsVisited return ErrNoObjectsVisited
} }
...@@ -449,3 +460,13 @@ func findMatchingInfo(target *resource.Info, infos []*resource.Info) (*resource. ...@@ -449,3 +460,13 @@ func findMatchingInfo(target *resource.Info, infos []*resource.Info) (*resource.
} }
return nil, false 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
}
...@@ -26,8 +26,10 @@ import ( ...@@ -26,8 +26,10 @@ import (
"testing" "testing"
"k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
api "k8s.io/kubernetes/pkg/api/v1" api "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/api/validation"
"k8s.io/kubernetes/pkg/client/unversioned/fake" "k8s.io/kubernetes/pkg/client/unversioned/fake"
"k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
...@@ -67,12 +69,13 @@ func TestUpdateResource(t *testing.T) { ...@@ -67,12 +69,13 @@ func TestUpdateResource(t *testing.T) {
func TestPerform(t *testing.T) { func TestPerform(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
namespace string namespace string
reader io.Reader reader io.Reader
count int count int
err bool swaggerFile string
errMessage string err bool
errMessage string
}{ }{
{ {
name: "Valid input", name: "Valid input",
...@@ -85,6 +88,13 @@ func TestPerform(t *testing.T) { ...@@ -85,6 +88,13 @@ func TestPerform(t *testing.T) {
reader: strings.NewReader(""), reader: strings.NewReader(""),
err: true, err: true,
errMessage: "no objects visited", 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) { ...@@ -105,6 +115,16 @@ func TestPerform(t *testing.T) {
c.ClientForMapping = func(mapping *meta.RESTMapping) (resource.RESTClient, error) { c.ClientForMapping = func(mapping *meta.RESTMapping) (resource.RESTClient, error) {
return &fake.RESTClient{}, nil 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) err := perform(c, tt.namespace, tt.reader, fn)
if (err != nil) != tt.err { if (err != nil) != tt.err {
...@@ -159,6 +179,14 @@ spec: ...@@ -159,6 +179,14 @@ spec:
targetPort: 9376 targetPort: 9376
` `
const testInvalidServiceManifest = `
kind: Service
apiVersion: v1
spec:
ports:
- port: "80"
`
const testEndpointManifest = ` const testEndpointManifest = `
kind: Endpoints kind: Endpoints
apiVersion: v1 apiVersion: v1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment