diff --git a/metadata/app/schema.go b/metadata/app/schema.go index 570cd971a6e7be0d4b7ceb8ed698326d2e1b8445..c3c720e9b6584631c0cf199567133461a96e5851 100644 --- a/metadata/app/schema.go +++ b/metadata/app/schema.go @@ -18,7 +18,9 @@ package app import ( "fmt" + "github.com/blang/semver" "github.com/ghodss/yaml" + "github.com/pkg/errors" ) const ( @@ -46,6 +48,20 @@ type Spec struct { License string `json:"license,omitempty"` } +func Unmarshal(bytes []byte) (*Spec, error) { + schema := Spec{} + err := yaml.Unmarshal(bytes, &schema) + if err != nil { + return nil, err + } + + if err = schema.validate(); err != nil { + return nil, err + } + + return &schema, nil +} + func (s *Spec) Marshal() ([]byte, error) { return yaml.Marshal(s) } @@ -74,6 +90,21 @@ func (s *Spec) AddRegistryRef(registryRefSpec *RegistryRefSpec) error { return nil } +func (s *Spec) validate() error { + compatVer, _ := semver.Make(DefaultAPIVersion) + ver, err := semver.Make(s.APIVersion) + if err != nil { + return errors.Wrap(err, "Failed to parse version in app spec") + } else if compatVer.Compare(ver) != 0 { + return fmt.Errorf( + "Current app uses unsupported spec version '%s' (this client only supports %s)", + s.APIVersion, + DefaultAPIVersion) + } + + return nil +} + type RepositorySpec struct { Type string `json:"type"` URI string `json:"uri"` diff --git a/metadata/app/schema_test.go b/metadata/app/schema_test.go index 9e458cef1ff30e08947e67ba8e5f9b12b5c37c02..c56112d48565e3d488fd0860ae8fe9eeeafbc6b1 100644 --- a/metadata/app/schema_test.go +++ b/metadata/app/schema_test.go @@ -18,6 +18,8 @@ package app import ( "fmt" "testing" + + "github.com/blang/semver" ) func makeSimpleRefSpec(name, protocol, uri, version string) *RegistryRefSpec { @@ -32,6 +34,48 @@ func makeSimpleRefSpec(name, protocol, uri, version string) *RegistryRefSpec { } } +func TestApiVersionValidate(t *testing.T) { + type spec struct { + spec string + err bool + } + tests := []spec{ + // Versions that we accept. + {spec: "0.0.1", err: false}, + {spec: "0.0.1+build.1", err: false}, + + // Other versions. + {spec: "0.0.0", err: true}, + {spec: "0.1.0", err: true}, + {spec: "1.0.0", err: true}, + + // Builds and pre-releases of current version. + {spec: "0.0.1-alpha", err: true}, + {spec: "0.0.1-beta+build.2", err: true}, + + // Other versions. + {spec: "0.1.0-alpha", err: true}, + {spec: "0.1.0+build.1", err: true}, + {spec: "0.1.0-beta+build.2", err: true}, + {spec: "1.0.0-alpha", err: true}, + {spec: "1.0.0+build.1", err: true}, + {spec: "1.0.0-beta+build.2", err: true}, + } + + for _, test := range tests { + _, err := semver.Make(test.spec) + if err != nil { + t.Errorf("Failed to parse version '%s':\n%v", test.spec, err) + } + + spec := &Spec{APIVersion: test.spec} + err = spec.validate() + if (test.err && err == nil) || (!test.err && err != nil) { + t.Errorf("Expected error for version '%s'? %t. Value of error: '%v'", test.spec, test.err, err) + } + } +} + func TestGetRegistryRefSuccess(t *testing.T) { example1 := Spec{ Registries: RegistryRefSpecs{ diff --git a/metadata/manager.go b/metadata/manager.go index 4aab3ffeff6dc5e9c25673a2ee52132107ac8ebb..061b588d476d3aceb22bcbae0683dc4d7293e081 100644 --- a/metadata/manager.go +++ b/metadata/manager.go @@ -21,7 +21,6 @@ import ( "path" "path/filepath" - "github.com/ghodss/yaml" "github.com/ksonnet/ksonnet/metadata/app" param "github.com/ksonnet/ksonnet/metadata/params" "github.com/ksonnet/ksonnet/metadata/registry" @@ -243,8 +242,7 @@ func (m *manager) AppSpec() (*app.Spec, error) { return nil, err } - schema := app.Spec{} - err = yaml.Unmarshal(bytes, &schema) + schema, err := app.Unmarshal(bytes) if err != nil { return nil, err } @@ -261,7 +259,7 @@ func (m *manager) AppSpec() (*app.Spec, error) { schema.Libraries = app.LibraryRefSpecs{} } - return &schema, nil + return schema, nil } func (m *manager) createUserDirTree() error { diff --git a/metadata/parts/schema.go b/metadata/parts/schema.go index c7f14ac16a9bc705ec1a55a50a8b05cb42751ffa..dde51272086a88aa544db0344cf1b992e260ae06 100644 --- a/metadata/parts/schema.go +++ b/metadata/parts/schema.go @@ -16,11 +16,15 @@ package parts import ( + "fmt" + + "github.com/blang/semver" "github.com/ghodss/yaml" + "github.com/pkg/errors" ) const ( - DefaultApiVersion = "0.1" + DefaultAPIVersion = "0.0.1" DefaultKind = "ksonnet.io/parts" ) @@ -41,10 +45,40 @@ type Spec struct { License string `json:"license"` } +func Unmarshal(bytes []byte) (*Spec, error) { + schema := Spec{} + err := yaml.Unmarshal(bytes, &schema) + if err != nil { + return nil, err + } + + if err = schema.validate(); err != nil { + return nil, err + } + + return &schema, nil +} + func (s *Spec) Marshal() ([]byte, error) { return yaml.Marshal(s) } +func (s *Spec) validate() error { + compatVer, _ := semver.Make(DefaultAPIVersion) + ver, err := semver.Make(s.APIVersion) + if err != nil { + return errors.Wrap(err, "Failed to parse version in app spec") + } else if compatVer.Compare(ver) != 0 { + return fmt.Errorf( + "Library '%s' uses unsupported spec version '%s' (this client only supports %s)", + s.Name, + s.APIVersion, + DefaultAPIVersion) + } + + return nil +} + type ContributorSpec struct { Name string `json:"name"` Email string `json:"email"` diff --git a/metadata/parts/schema_test.go b/metadata/parts/schema_test.go new file mode 100644 index 0000000000000000000000000000000000000000..11b55e1cc62b321b0e2a9fa361cfbe5f8aed4f43 --- /dev/null +++ b/metadata/parts/schema_test.go @@ -0,0 +1,64 @@ +// Copyright 2017 The kubecfg authors +// +// +// 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 parts + +import ( + "testing" + + "github.com/blang/semver" +) + +func TestApiVersionValidate(t *testing.T) { + type spec struct { + spec string + err bool + } + tests := []spec{ + // Versions that we accept. + {spec: "0.0.1", err: false}, + {spec: "0.0.1+build.1", err: false}, + + // Other versions. + {spec: "0.0.0", err: true}, + {spec: "0.1.0", err: true}, + {spec: "1.0.0", err: true}, + + // Builds and pre-releases of current version. + {spec: "0.0.1-alpha", err: true}, + {spec: "0.0.1-beta+build.2", err: true}, + + // Other versions. + {spec: "0.1.0-alpha", err: true}, + {spec: "0.1.0+build.1", err: true}, + {spec: "0.1.0-beta+build.2", err: true}, + {spec: "1.0.0-alpha", err: true}, + {spec: "1.0.0+build.1", err: true}, + {spec: "1.0.0-beta+build.2", err: true}, + } + + for _, test := range tests { + _, err := semver.Make(test.spec) + if err != nil { + t.Errorf("Failed to parse version '%s':\n%v", test.spec, err) + } + + spec := &Spec{APIVersion: test.spec} + err = spec.validate() + if (test.err && err == nil) || (!test.err && err != nil) { + t.Errorf("Expected error for version '%s'? %t. Value of error: '%v'", test.spec, test.err, err) + } + } +} diff --git a/metadata/registry.go b/metadata/registry.go index 6eb5dac02ddd587b907681ac1e28ab5b5fc1d7ad..42e6a54432d4a121e046a5ce585e572b792d6e09 100644 --- a/metadata/registry.go +++ b/metadata/registry.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" - "github.com/ghodss/yaml" "github.com/ksonnet/ksonnet/metadata/app" "github.com/ksonnet/ksonnet/metadata/parts" "github.com/ksonnet/ksonnet/metadata/registry" @@ -125,8 +124,7 @@ func (m *manager) GetDependency(libName string) (*parts.Spec, error) { return nil, err } - var partsSpec parts.Spec - err = yaml.Unmarshal(partsBytes, &partsSpec) + partsSpec, err := parts.Unmarshal(partsBytes) if err != nil { return nil, err } @@ -140,7 +138,7 @@ func (m *manager) GetDependency(libName string) (*parts.Spec, error) { partsSpec.Prototypes = append(partsSpec.Prototypes, protoSpec.Name) } - return &partsSpec, nil + return partsSpec, nil } func (m *manager) CacheDependency(registryName, libID, libName, libVersion string) (*parts.Spec, error) { @@ -335,12 +333,11 @@ func (m *manager) registrySpecFromFile(path AbsPath) (*registry.Spec, bool, erro return nil, false, err } - registrySpec := registry.Spec{} - err = yaml.Unmarshal(registrySpecBytes, ®istrySpec) + registrySpec, err := registry.Unmarshal(registrySpecBytes) if err != nil { return nil, false, err } - return ®istrySpec, true, nil + return registrySpec, true, nil } return nil, false, nil diff --git a/metadata/registry/schema.go b/metadata/registry/schema.go index a4ce51ffa6e194bc54fc1d93f6522b512c6219b5..5c91de3b6a4fdf130e40ccb3b09f683904b26c4f 100644 --- a/metadata/registry/schema.go +++ b/metadata/registry/schema.go @@ -16,12 +16,16 @@ package registry import ( + "fmt" + + "github.com/blang/semver" "github.com/ghodss/yaml" "github.com/ksonnet/ksonnet/metadata/app" + "github.com/pkg/errors" ) const ( - DefaultApiVersion = "0.1" + DefaultAPIVersion = "0.1.0" DefaultKind = "ksonnet.io/registry" ) @@ -32,10 +36,45 @@ type Spec struct { Libraries LibraryRefSpecs `json:"libraries"` } +func Unmarshal(bytes []byte) (*Spec, error) { + schema := Spec{} + err := yaml.Unmarshal(bytes, &schema) + if err != nil { + return nil, err + } + + if err = schema.validate(); err != nil { + return nil, err + } + + return &schema, nil +} + func (s *Spec) Marshal() ([]byte, error) { return yaml.Marshal(s) } +func (s *Spec) validate() error { + // Originally, the default value for `apiVersion` was `0.1`. This is not a + // valid semver, so before we do anything, we need to convert it to one. + if s.APIVersion == "0.1" { + s.APIVersion = "0.1.0" + } + + compatVer, _ := semver.Make(DefaultAPIVersion) + ver, err := semver.Make(s.APIVersion) + if err != nil { + return errors.Wrap(err, "Failed to parse version in app spec") + } else if compatVer.Compare(ver) != 0 { + return fmt.Errorf( + "Registry uses unsupported spec version '%s' (this client only supports %s)", + s.APIVersion, + DefaultAPIVersion) + } + + return nil +} + type Specs []*Spec type LibraryRef struct { diff --git a/metadata/registry/schema_test.go b/metadata/registry/schema_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4ceb2bb6933c4ab32c41967b83e01fe511142217 --- /dev/null +++ b/metadata/registry/schema_test.go @@ -0,0 +1,64 @@ +// Copyright 2017 The kubecfg authors +// +// +// 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 registry + +import ( + "testing" + + "github.com/blang/semver" +) + +func TestApiVersionValidate(t *testing.T) { + type spec struct { + spec string + err bool + } + tests := []spec{ + // Versions that we accept. + {spec: "0.1.0", err: false}, + {spec: "0.1.0+build.1", err: false}, + + // Other versions. + {spec: "0.0.0", err: true}, + {spec: "0.0.1", err: true}, + {spec: "1.0.0", err: true}, + + // Builds and pre-releases of current version. + {spec: "0.1.0-alpha", err: true}, + {spec: "0.1.0-beta+build.2", err: true}, + + // Other versions. + {spec: "0.0.1-alpha", err: true}, + {spec: "0.0.1+build.1", err: true}, + {spec: "0.0.1-beta+build.2", err: true}, + {spec: "1.0.0-alpha", err: true}, + {spec: "1.0.0+build.1", err: true}, + {spec: "1.0.0-beta+build.2", err: true}, + } + + for _, test := range tests { + _, err := semver.Make(test.spec) + if err != nil { + t.Errorf("Failed to parse version '%s':\n%v", test.spec, err) + } + + spec := &Spec{APIVersion: test.spec} + err = spec.validate() + if (test.err && err == nil) || (!test.err && err != nil) { + t.Errorf("Expected error for version '%s'? %t. Value of error: '%v'", test.spec, test.err, err) + } + } +} diff --git a/metadata/registry_managers.go b/metadata/registry_managers.go index 97c48168c3ebf8b8d92ce702a6412c363d0c81fc..9a8d5ac4b395ccf517df370e9b264a3e12058488 100644 --- a/metadata/registry_managers.go +++ b/metadata/registry_managers.go @@ -9,7 +9,6 @@ import ( "path" "strings" - "github.com/ghodss/yaml" "github.com/google/go-github/github" "github.com/ksonnet/ksonnet/metadata/app" "github.com/ksonnet/ksonnet/metadata/parts" @@ -120,8 +119,7 @@ func (gh *gitHubRegistryManager) FetchRegistrySpec() (*registry.Spec, error) { } // Deserialize, return. - registrySpec := registry.Spec{} - err = yaml.Unmarshal([]byte(registrySpecText), ®istrySpec) + registrySpec, err := registry.Unmarshal([]byte(registrySpecText)) if err != nil { return nil, err } @@ -131,7 +129,7 @@ func (gh *gitHubRegistryManager) FetchRegistrySpec() (*registry.Spec, error) { CommitSHA: gh.GitVersion.CommitSHA, } - return ®istrySpec, nil + return registrySpec, nil } func (gh *gitHubRegistryManager) MakeRegistryRefSpec() *app.RegistryRefSpec { @@ -164,13 +162,12 @@ func (gh *gitHubRegistryManager) ResolveLibrarySpec(libID, libRefSpec string) (* return nil, err } - parts := parts.Spec{} - err = yaml.Unmarshal([]byte(partsSpecText), &parts) + parts, err := parts.Unmarshal([]byte(partsSpecText)) if err != nil { return nil, err } - return &parts, nil + return parts, nil } func (gh *gitHubRegistryManager) ResolveLibrary(libID, libAlias, libRefSpec string, onFile registry.ResolveFile, onDir registry.ResolveDirectory) (*parts.Spec, *app.LibraryRefSpec, error) { @@ -213,8 +210,7 @@ func (gh *gitHubRegistryManager) ResolveLibrary(libID, libAlias, libRefSpec stri return nil, nil, err } - parts := parts.Spec{} - err = yaml.Unmarshal([]byte(partsSpecText), &parts) + parts, err := parts.Unmarshal([]byte(partsSpecText)) if err != nil { return nil, nil, err } @@ -228,7 +224,7 @@ func (gh *gitHubRegistryManager) ResolveLibrary(libID, libAlias, libRefSpec stri }, } - return &parts, &refSpec, nil + return parts, &refSpec, nil } func (gh *gitHubRegistryManager) resolveDir(client *github.Client, libID, path, version string, onFile registry.ResolveFile, onDir registry.ResolveDirectory) error { diff --git a/metadata/registry_test.go b/metadata/registry_test.go index f69a31a47e48cb8196571c030d3e8752c0c2aa82..5c8926867ebd4c10321d76b2f06c955d66c818fa 100644 --- a/metadata/registry_test.go +++ b/metadata/registry_test.go @@ -188,7 +188,7 @@ func (m *mockRegistryManager) RegistrySpecFilePath() string { func (m *mockRegistryManager) FetchRegistrySpec() (*registry.Spec, error) { registrySpec := registry.Spec{ - APIVersion: registry.DefaultApiVersion, + APIVersion: registry.DefaultAPIVersion, Kind: registry.DefaultKind, } diff --git a/prototype/interface.go b/prototype/interface.go index 782884b6e5ae6993dc64007436c73f431b8db1f4..e4a7d00e3ec43eea86c97a81ea45706fef589a3d 100644 --- a/prototype/interface.go +++ b/prototype/interface.go @@ -11,6 +11,10 @@ func Unmarshal(bytes []byte) (*SpecificationSchema, error) { return nil, err } + if err = p.validate(); err != nil { + return nil, err + } + return &p, nil } diff --git a/prototype/prototype_test.go b/prototype/prototype_test.go index d187031d05a9dda4ca8d181e4f75b31998d892a1..620696300c79000751bb5d64599942414a34ba31 100644 --- a/prototype/prototype_test.go +++ b/prototype/prototype_test.go @@ -3,6 +3,8 @@ package prototype import ( "sort" "testing" + + "github.com/blang/semver" ) const ( @@ -10,7 +12,7 @@ const ( ) var simpleService = `{ - "apiVersion": "0.1", + "apiVersion": "0.0.1", "name": "io.some-vendor.pkg.simple-service", "template": { "description": "Generates a simple service with a port exposed", @@ -28,7 +30,7 @@ var simpleService = `{ }` var simpleDeployment = `{ - "apiVersion": "0.1", + "apiVersion": "0.0.1", "name": "io.some-vendor.pkg.simple-deployment", "template": { "description": "Generates a simple service with a port exposed", @@ -62,7 +64,7 @@ func assertProp(t *testing.T, name string, expected string, actual string) { func TestSimpleUnmarshal(t *testing.T) { p := unmarshal(t, []byte(simpleService)) - assertProp(t, "apiVersion", p.APIVersion, "0.1") + assertProp(t, "apiVersion", p.APIVersion, "0.0.1") assertProp(t, "name", p.Name, "io.some-vendor.pkg.simple-service") assertProp(t, "description", p.Template.Description, "Generates a simple service with a port exposed") } @@ -147,3 +149,45 @@ func TestSearch(t *testing.T) { }) assertSearch(t, idx, Substring, "foo", []string{}) } + +func TestApiVersionValidate(t *testing.T) { + type spec struct { + spec string + err bool + } + tests := []spec{ + // Versions that we accept. + {spec: "0.0.1", err: false}, + {spec: "0.0.1+build.1", err: false}, + + // Other versions. + {spec: "0.0.0", err: true}, + {spec: "0.1.0", err: true}, + {spec: "1.0.0", err: true}, + + // Builds and pre-releases of current version. + {spec: "0.0.1-alpha", err: true}, + {spec: "0.0.1-beta+build.2", err: true}, + + // Other versions. + {spec: "0.1.0-alpha", err: true}, + {spec: "0.1.0+build.1", err: true}, + {spec: "0.1.0-beta+build.2", err: true}, + {spec: "1.0.0-alpha", err: true}, + {spec: "1.0.0+build.1", err: true}, + {spec: "1.0.0-beta+build.2", err: true}, + } + + for _, test := range tests { + _, err := semver.Make(test.spec) + if err != nil { + t.Errorf("Failed to parse version '%s':\n%v", test.spec, err) + } + + spec := &SpecificationSchema{APIVersion: test.spec} + err = spec.validate() + if (test.err && err == nil) || (!test.err && err != nil) { + t.Errorf("Expected error for version '%s'? %t. Value of error: '%v'", test.spec, test.err, err) + } + } +} diff --git a/prototype/specification.go b/prototype/specification.go index 165bdcf543eaaadd438281d69c79146899494023..056ccbe136d62c617dd1553a75311be2df61fbb3 100644 --- a/prototype/specification.go +++ b/prototype/specification.go @@ -7,7 +7,9 @@ import ( "strconv" "strings" + "github.com/blang/semver" "github.com/ksonnet/ksonnet/utils" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -18,6 +20,8 @@ import ( // const ( + DefaultAPIVersion = "0.0.1" + apiVersionTag = "@apiVersion" nameTag = "@name" descriptionTag = "@description" @@ -135,6 +139,21 @@ type SpecificationSchema struct { Template SnippetSchema `json:"template"` } +func (s *SpecificationSchema) validate() error { + compatVer, _ := semver.Make(DefaultAPIVersion) + ver, err := semver.Make(s.APIVersion) + if err != nil { + return errors.Wrap(err, "Failed to parse version in app spec") + } else if compatVer.Compare(ver) != 0 { + return fmt.Errorf( + "Current app uses unsupported spec version '%s' (this client only supports %s)", + s.APIVersion, + DefaultAPIVersion) + } + + return nil +} + // SpecificationSchemas is a slice of pointer to `SpecificationSchema`. type SpecificationSchemas []*SpecificationSchema