Commit 323836ea authored by Dave Cunningham's avatar Dave Cunningham

JSONschema support

parent 86e61385
...@@ -22,7 +22,6 @@ import ( ...@@ -22,7 +22,6 @@ import (
"fmt" "fmt"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"log" "log"
"os"
"os/exec" "os/exec"
"github.com/kubernetes/helm/pkg/expansion" "github.com/kubernetes/helm/pkg/expansion"
...@@ -50,7 +49,11 @@ type expandyBirdOutput struct { ...@@ -50,7 +49,11 @@ type expandyBirdOutput struct {
// expanded configuration as a string on success. // expanded configuration as a string on success.
func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.ServiceResponse, error) { func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.ServiceResponse, error) {
err := expansion.ValidateRequest(request) if err := expansion.ValidateRequest(request); err != nil {
return nil, err
}
request, err := expansion.ValidateProperties(request)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -65,23 +68,15 @@ func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.Se ...@@ -65,23 +68,15 @@ func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.Se
} }
entrypointIndex := -1 entrypointIndex := -1
schemaIndex := -1
for i, f := range chartMembers { for i, f := range chartMembers {
if f.Path == chartFile.Expander.Entrypoint { if f.Path == chartFile.Expander.Entrypoint {
entrypointIndex = i entrypointIndex = i
} }
if f.Path == chartFile.Schema {
schemaIndex = i
}
} }
if entrypointIndex == -1 { if entrypointIndex == -1 {
message := fmt.Sprintf("The entrypoint in the chart.yaml cannot be found: %s", chartFile.Expander.Entrypoint) message := fmt.Sprintf("The entrypoint in the chart.yaml cannot be found: %s", chartFile.Expander.Entrypoint)
return nil, fmt.Errorf("%s: %s", chartInv.Name, message) return nil, fmt.Errorf("%s: %s", chartInv.Name, message)
} }
if chartFile.Schema != "" && schemaIndex == -1 {
message := fmt.Sprintf("The schema in the chart.yaml cannot be found: %s", chartFile.Schema)
return nil, fmt.Errorf("%s: %s", chartInv.Name, message)
}
// Those are automatically increasing buffers, so writing arbitrary large // Those are automatically increasing buffers, so writing arbitrary large
// data here won't block the child process. // data here won't block the child process.
...@@ -104,20 +99,12 @@ func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.Se ...@@ -104,20 +99,12 @@ func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.Se
Stderr: &stderr, Stderr: &stderr,
} }
if chartFile.Schema != "" {
// appending to exsiting Env is required
cmd.Env = append(os.Environ(), "VALIDATE_SCHEMA=1")
}
for i, f := range chartMembers { for i, f := range chartMembers {
name := f.Path name := f.Path
path := f.Path path := f.Path
if i == entrypointIndex { if i == entrypointIndex {
// This is how expandyBird identifies the entrypoint. // This is how expandyBird identifies the entrypoint.
name = chartInv.Type name = chartInv.Type
} else if i == schemaIndex {
// Doesn't matter what it was originally called, expandyBird expects to find it here.
name = chartInv.Type + ".schema"
} }
cmd.Args = append(cmd.Args, name, path, string(f.Content)) cmd.Args = append(cmd.Args, name, path, string(f.Content))
} }
......
...@@ -478,7 +478,7 @@ func TestSchemaFail(t *testing.T) { ...@@ -478,7 +478,7 @@ func TestSchemaFail(t *testing.T) {
}, },
}, },
nil, // Response. nil, // Response.
"Invalid properties for", `"prop2" property is missing and required`,
) )
} }
......
...@@ -90,7 +90,10 @@ func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.Se ...@@ -90,7 +90,10 @@ func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.Se
return nil, err return nil, err
} }
// TODO(dcunnin): Validate via JSONschema. request, err = expansion.ValidateProperties(request)
if err != nil {
return nil, err
}
chartInv := request.ChartInvocation chartInv := request.ChartInvocation
chartMembers := request.Chart.Members chartMembers := request.Chart.Members
......
hash: a72f13699934fc94c8df5b56c76fc66d2279b2a4ff1396c2f1bff944456a8adf hash: f9b47a1852e40963671d923d7170aa4d730f7d74de94a3c66420a3c1635bc5c5
updated: 2016-03-30T23:15:39.980396345-04:00 updated: 2016-04-01T21:35:30.235550603-04:00
imports: imports:
- name: github.com/aokoli/goutils - name: github.com/aokoli/goutils
version: 45307ec16e3cd47cd841506c081f7afd8237d210 version: 45307ec16e3cd47cd841506c081f7afd8237d210
- name: github.com/cloudfoundry-incubator/candiedyaml - name: github.com/cloudfoundry-incubator/candiedyaml
version: 479485e9bfc69ee37d074b36ce36da5e4fba7941 version: 479485e9bfc69ee37d074b36ce36da5e4fba7941
- name: github.com/codegangsta/cli - name: github.com/codegangsta/cli
version: a2943485b110df8842045ae0600047f88a3a56a1 version: bc465becccd1d527002fda095fc3c19d9c115029
- name: github.com/emicklei/go-restful - name: github.com/emicklei/go-restful
version: b86acf97a74ed7603ac78d012f5535b4d587b156 version: 402f11d42bfe18198ffd5c68258c631c8fbf2c3c
subpackages: subpackages:
- log - log
- name: github.com/ghodss/yaml - name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee version: 1a6f069841556a7bcaff4a397ca6e8328d266c2f
- name: github.com/google/go-github - name: github.com/google/go-github
version: b8b4ac742977310ff6e75140a403a38dab109977 version: 4403af9a2a0f2c2577be18a928d98f77d5748168
subpackages: subpackages:
- github - github
- name: github.com/gorilla/context - name: github.com/gorilla/context
version: 1c83b3eabd45b6d76072b66b746c20815fb2872d version: 1ea25387ff6f684839d82767c1733ff4d4d15d0a
- name: github.com/gorilla/handlers - name: github.com/gorilla/handlers
version: ee54c7b44cab12289237fb8631314790076e728b version: ee54c7b44cab12289237fb8631314790076e728b
- name: github.com/gorilla/mux - name: github.com/gorilla/mux
version: 26a6070f849969ba72b72256e9f14cf519751690 version: 0eeaf8392f5b04950925b8a69fe70f110fa7cbfc
- name: github.com/juju/gojsonpointer
version: afe8b77aa08f272b49e01b82de78510c11f61500
- name: github.com/juju/gojsonreference
version: f0d24ac5ee330baa21721cdff56d45e4ee42628e
- name: github.com/juju/gojsonschema
version: e1ad140384f254c82f89450d9a7c8dd38a632838
- name: github.com/Masterminds/httputil - name: github.com/Masterminds/httputil
version: e9b977e9cf16f9d339573e18f0f1f7ce5d3f419a version: e9b977e9cf16f9d339573e18f0f1f7ce5d3f419a
- name: github.com/Masterminds/semver - name: github.com/Masterminds/semver
...@@ -30,39 +36,35 @@ imports: ...@@ -30,39 +36,35 @@ imports:
- name: github.com/Masterminds/sprig - name: github.com/Masterminds/sprig
version: 679bb747f11c6ffc3373965988fea8877c40b47b version: 679bb747f11c6ffc3373965988fea8877c40b47b
- name: golang.org/x/net - name: golang.org/x/net
version: 04b9de9b512f58addf28c9853d50ebef61c3953e version: 3e8a7b0329d536af18e227bb21b6da4d1dbbe180
subpackages: subpackages:
- context - context
- context/ctxhttp - context/ctxhttp
- name: golang.org/x/oauth2 - name: golang.org/x/oauth2
version: 8a57ed94ffd43444c0879fe75701732a38afc985 version: 33fa30fe45020622640e947917fd1fc4c81e3dce
subpackages: subpackages:
- google - google
- internal - internal
- jws - jws
- jwt - jwt
- name: google.golang.org/api - name: google.golang.org/api
version: 0caa37974a5f5ae67172acf68b4970f7864f994c version: 43c645d4bcf9251ced36c823a93b6d198764aae4
subpackages: subpackages:
- storage/v1 - storage/v1
- gensupport - gensupport
- googleapi - googleapi
- googleapi/internal/uritemplates - googleapi/internal/uritemplates
- name: google.golang.org/appengine
version: a503df954af258b9a70918df2a524d6a85ecefdb
subpackages:
- urlfetch
- name: google.golang.org/cloud - name: google.golang.org/cloud
version: fb10e8da373d97f6ba5e648299a10b3b91f14cd5 version: 8a7fce32d2cdf2d4e19068ecc53164b973b3e958
subpackages: subpackages:
- compute/metadata - compute/metadata
- internal - internal
- name: gopkg.in/mgo.v2 - name: gopkg.in/mgo.v2
version: d90005c5262a3463800497ea5a89aed5fe22c886 version: b6e2fa371e64216a45e61072a96d4e3859f169da
subpackages: subpackages:
- bson - bson
- internal/sasl - internal/sasl
- internal/scram - internal/scram
- name: gopkg.in/yaml.v2 - name: gopkg.in/yaml.v2
version: f7716cbe52baa25d2e9b0d0da546fcf909fc16b4 version: a83829b6f1293c91addabc89d0571c246397bbf4
devImports: [] devImports: []
...@@ -21,3 +21,4 @@ import: ...@@ -21,3 +21,4 @@ import:
- package: github.com/Masterminds/sprig - package: github.com/Masterminds/sprig
version: ^2.1.0 version: ^2.1.0
- package: github.com/Masterminds/httputil - package: github.com/Masterminds/httputil
- package: github.com/juju/gojsonschema
...@@ -17,9 +17,12 @@ limitations under the License. ...@@ -17,9 +17,12 @@ limitations under the License.
package expansion package expansion
import ( import (
"github.com/kubernetes/helm/pkg/chart" "bytes"
"fmt" "fmt"
"github.com/ghodss/yaml"
"github.com/juju/gojsonschema"
"github.com/kubernetes/helm/pkg/chart"
) )
// ValidateRequest does basic sanity checks on the request. // ValidateRequest does basic sanity checks on the request.
...@@ -50,3 +53,74 @@ func ValidateRequest(request *ServiceRequest) error { ...@@ -50,3 +53,74 @@ func ValidateRequest(request *ServiceRequest) error {
return nil return nil
} }
// ValidateProperties validates the properties in the chart invocation against the schema file in
// the chart itself, which is assumed to be JSONschema. It also modifies a copy of the request to
// add defaults values if properties are not provided (according to the default field in
// JSONschema), and returns this copy.
func ValidateProperties(request *ServiceRequest) (*ServiceRequest, error) {
schemaFilename := request.Chart.Chartfile.Schema
if schemaFilename == "" {
// No schema, so perform no validation.
return request, nil
}
chartInv := request.ChartInvocation
var schemaBytes *[]byte
for _, f := range request.Chart.Members {
if f.Path == schemaFilename {
schemaBytes = &f.Content
}
}
if schemaBytes == nil {
return nil, fmt.Errorf("%s: The schema referenced from the Chart.yaml cannot be found: %s",
chartInv.Name, schemaFilename)
}
var schemaDoc interface{}
if err := yaml.Unmarshal(*schemaBytes, &schemaDoc); err != nil {
return nil, fmt.Errorf("%s: %s was not valid YAML: %v",
chartInv.Name, schemaFilename, err)
}
// Build a schema object
schema, err := gojsonschema.NewSchema(gojsonschema.NewGoLoader(schemaDoc))
if err != nil {
return nil, err
}
// Do validation
result, err := schema.Validate(gojsonschema.NewGoLoader(request.ChartInvocation.Properties))
if err != nil {
return nil, err
}
// Need to concat errors here
if !result.Valid() {
var message bytes.Buffer
message.WriteString("Properties failed validation:\n")
for _, err := range result.Errors() {
message.WriteString(fmt.Sprintf("- %s", err))
}
return nil, fmt.Errorf("%s: %s", chartInv.Name, message.String())
}
// Fill in defaults (after validation).
modifiedProperties, err := schema.InsertDefaults(request.ChartInvocation.Properties)
if err != nil {
return nil, err
}
modifiedResource := *request.ChartInvocation
modifiedResource.Properties = modifiedProperties
modifiedRequest := &ServiceRequest{
ChartInvocation: &modifiedResource,
Chart: request.Chart,
}
return modifiedRequest, nil
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package expansion
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
)
func testPropertiesValidation(t *testing.T, req *ServiceRequest, expRequest *ServiceRequest, expError string) {
modifiedRequest, err := ValidateProperties(req)
if err != nil {
message := err.Error()
if expRequest != nil || !strings.Contains(message, expError) {
t.Fatalf("unexpected error: %v\n", err)
}
} else {
if expRequest == nil {
t.Fatalf("expected error did not occur: %s\n", expError)
}
if !reflect.DeepEqual(modifiedRequest, expRequest) {
message := fmt.Sprintf("want:\n%s\nhave:\n%s\n", expRequest, modifiedRequest)
t.Fatalf("output mismatch:\n%s\n", message)
}
}
}
func TestNoSchema(t *testing.T) {
req := &ServiceRequest{
ChartInvocation: &common.Resource{
Properties: map[string]interface{}{
"prop1": 3.0,
"prop2": "foo",
},
},
Chart: &chart.Content{
Chartfile: &chart.Chartfile{},
Members: []*chart.Member{},
},
}
testPropertiesValidation(t, req, req, "") // Returns it unchanged.
}
func TestSchemaNotFound(t *testing.T) {
testPropertiesValidation(
t,
&ServiceRequest{
ChartInvocation: &common.Resource{
Properties: map[string]interface{}{
"prop1": 3.0,
"prop2": "foo",
},
},
Chart: &chart.Content{
Chartfile: &chart.Chartfile{
Schema: "Schema.yaml",
},
},
},
nil, // No response to check.
"The schema referenced from the Chart.yaml cannot be found: Schema.yaml",
)
}
var schemaContent = []byte(`
required: ["prop2"]
additionalProperties: false
properties:
prop1:
description: Nice description.
type: integer
default: 42
prop2:
description: Nice description.
type: string
`)
func TestSchema(t *testing.T) {
req := &ServiceRequest{
ChartInvocation: &common.Resource{
Properties: map[string]interface{}{
"prop1": 3.0,
"prop2": "foo",
},
},
Chart: &chart.Content{
Chartfile: &chart.Chartfile{
Schema: "Schema.yaml",
},
Members: []*chart.Member{
{
Path: "Schema.yaml",
Content: schemaContent,
},
},
},
}
// No defaults, returns it unchanged:
testPropertiesValidation(t, req, req, "")
}
func TestBadProperties(t *testing.T) {
testPropertiesValidation(
t,
&ServiceRequest{
ChartInvocation: &common.Resource{
Properties: map[string]interface{}{
"prop1": 3.0,
"prop3": map[string]interface{}{},
},
},
Chart: &chart.Content{
Chartfile: &chart.Chartfile{
Schema: "Schema.yaml",
},
Members: []*chart.Member{
{
Path: "Schema.yaml",
Content: schemaContent,
},
},
},
},
nil,
"Properties failed validation:",
)
}
func TestDefault(t *testing.T) {
testPropertiesValidation(
t,
&ServiceRequest{
ChartInvocation: &common.Resource{
Name: "TestName",
Type: "TestType",
Properties: map[string]interface{}{
"prop2": "ok",
},
},
Chart: &chart.Content{
Chartfile: &chart.Chartfile{
Schema: "Schema.yaml",
},
Members: []*chart.Member{
{
Path: "Schema.yaml",
Content: schemaContent,
},
},
},
},
&ServiceRequest{
ChartInvocation: &common.Resource{
Name: "TestName",
Type: "TestType",
Properties: map[string]interface{}{
"prop1": 42.0,
"prop2": "ok",
},
},
Chart: &chart.Content{
Chartfile: &chart.Chartfile{
Schema: "Schema.yaml",
},
Members: []*chart.Member{
{
Path: "Schema.yaml",
Content: schemaContent,
},
},
},
},
"", // Error
)
}
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