Skip to content

Commit

Permalink
Merge pull request #160 from ycombinator/semantic-validations
Browse files Browse the repository at this point in the history
Semantic validations
  • Loading branch information
ycombinator authored Apr 22, 2021
2 parents 0a25dbf + 63a6d6d commit 92ee6e4
Show file tree
Hide file tree
Showing 31 changed files with 326 additions and 33 deletions.
4 changes: 3 additions & 1 deletion code/go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ go 1.15

require (
github.com/Masterminds/semver/v3 v3.1.0
github.com/PaesslerAG/jsonpath v0.1.1
github.com/creasty/defaults v1.5.1
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901
github.com/pkg/errors v0.9.1
github.com/rakyll/statik v0.1.7
github.com/stretchr/testify v1.6.1
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415
github.com/xeipuuv/gojsonschema v1.2.0
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
)
)
7 changes: 7 additions & 0 deletions code/go/go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
github.com/creasty/defaults v1.5.1 h1:j8WexcS3d/t4ZmllX4GEkl4wIB/trOr035ajcLHCISM=
github.com/creasty/defaults v1.5.1/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package validator
package errors

import (
"fmt"
Expand All @@ -25,3 +25,15 @@ func (ve ValidationErrors) Error() string {

return message.String()
}

// Append adds more validation errors.
func (ve *ValidationErrors) Append(moreErrs ValidationErrors) {
if len(moreErrs) == 0 {
return
}

errs := *ve
errs = append(errs, moreErrs...)

*ve = errs
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package validator
package errors

import (
"errors"
Expand Down
83 changes: 83 additions & 0 deletions code/go/internal/pkgpath/files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package pkgpath

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/PaesslerAG/jsonpath"
"github.com/joeshaw/multierror"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)

// File represents a file in the package.
type File struct {
path string
os.FileInfo
}

// Files finds files for the given glob
func Files(glob string) ([]File, error) {
paths, err := filepath.Glob(glob)
if err != nil {
return nil, err
}

var errs multierror.Errors
var files = make([]File, 0)
for _, path := range paths {
info, err := os.Stat(path)
if err != nil {
errs = append(errs, err)
continue
}

file := File{path, info}
files = append(files, file)
}

return files, errs.Err()
}

// Values returns values within the file matching the given path. Paths
// should be expressed using JSONPath syntax. This method is only supported
// for YAML and JSON files.
func (f File) Values(path string) (interface{}, error) {
fileName := f.Name()
fileExt := strings.TrimLeft(filepath.Ext(fileName), ".")

if fileExt != "json" && fileExt != "yaml" && fileExt != "yml" {
return nil, fmt.Errorf("cannot extract values from file type = %s", fileExt)
}

contents, err := ioutil.ReadFile(f.path)
if err != nil {
return nil, errors.Wrap(err, "reading file content failed")
}

var v interface{}
if fileExt == "yaml" || fileExt == "yml" {
if err := yaml.Unmarshal(contents, &v); err != nil {
return nil, errors.Wrapf(err, "unmarshalling YAML file failed (path: %s)", fileName)
}
} else if fileExt == "json" {
if err := json.Unmarshal(contents, &v); err != nil {
return nil, errors.Wrapf(err, "unmarshalling JSON file failed (path: %s)", fileName)
}
}

return jsonpath.Get(path, v)
}

// Path returns the complete path to the file.
func (f File) Path() string {
return f.path
}
12 changes: 7 additions & 5 deletions code/go/internal/validator/folder_item_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import (
"regexp"
"sync"

ve "github.com/elastic/package-spec/code/go/internal/errors"

"github.com/pkg/errors"
"github.com/xeipuuv/gojsonschema"

"github.com/elastic/package-spec/code/go/internal/yamlschema"
)

const (
relativePathFormat = "relative-path"
relativePathFormat = "relative-path"
dataStreamNameFormat = "data-stream-name"
)

Expand Down Expand Up @@ -67,11 +69,11 @@ func (s *folderItemSpec) isSameType(file os.FileInfo) bool {
return false
}

func (s *folderItemSpec) validate(fs http.FileSystem, folderSpecPath string, itemPath string) ValidationErrors {
func (s *folderItemSpec) validate(fs http.FileSystem, folderSpecPath string, itemPath string) ve.ValidationErrors {
// loading item content
itemData, err := loadItemContent(itemPath, s.ContentMediaType)
if err != nil {
return ValidationErrors{err}
return ve.ValidationErrors{err}
}

var schemaLoader gojsonschema.JSONLoader
Expand All @@ -98,14 +100,14 @@ func (s *folderItemSpec) validate(fs http.FileSystem, folderSpecPath string, ite
loadDataStreamNameFormatChecker(filepath.Dir(itemPath))
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return ValidationErrors{err}
return ve.ValidationErrors{err}
}

if result.Valid() {
return nil // item content is valid according to the loaded schema
}

var errs ValidationErrors
var errs ve.ValidationErrors
for _, re := range result.Errors() {
errs = append(errs, fmt.Errorf("field %s: %s", re.Field(), adjustErrorDescription(re.Description())))
}
Expand Down
6 changes: 4 additions & 2 deletions code/go/internal/validator/folder_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"regexp"
"strings"

ve "github.com/elastic/package-spec/code/go/internal/errors"

"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -60,8 +62,8 @@ func newFolderSpec(fs http.FileSystem, specPath string) (*folderSpec, error) {
return &spec, nil
}

func (s *folderSpec) validate(packageName string, folderPath string) ValidationErrors {
var errs ValidationErrors
func (s *folderSpec) validate(packageName string, folderPath string) ve.ValidationErrors {
var errs ve.ValidationErrors
files, err := ioutil.ReadDir(folderPath)
if err != nil {
errs = append(errs, errors.Wrapf(err, "could not read folder [%s]", folderPath))
Expand Down
58 changes: 58 additions & 0 deletions code/go/internal/validator/semantic/kibana_matching_object_ids.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package semantic

import (
"fmt"
"path/filepath"
"strings"

ve "github.com/elastic/package-spec/code/go/internal/errors"

"github.com/elastic/package-spec/code/go/internal/pkgpath"
"github.com/pkg/errors"
)

// ValidateKibanaObjectIDs returns validation errors if there are any Kibana
// object files that define IDs not matching the file's name. That is, it returns
// validation errors if a Kibana object file, foo.json, in the package defines
// an object ID other than foo inside it.
func ValidateKibanaObjectIDs(pkgRoot string) ve.ValidationErrors {
var errs ve.ValidationErrors

filePaths := filepath.Join(pkgRoot, "kibana", "*", "*.json")
objectFiles, err := pkgpath.Files(filePaths)
if err != nil {
errs = append(errs, errors.Wrap(err, "error finding Kibana object files"))
return errs
}

for _, objectFile := range objectFiles {
filePath := objectFile.Path()

idPath := "$.id"
// Special case: object is of type 'security_rule'
if filepath.Base(filepath.Dir(filePath)) == "security_rule" {
idPath = "$.rule_id"
}

objectID, err := objectFile.Values(idPath)
if err != nil {
errs = append(errs, errors.Wrapf(err, "unable to get Kibana object ID in file [%s]", filePath))
continue
}

// fileID == filename without the extension == expected ID of Kibana object defined inside file.
fileName := filepath.Base(filePath)
fileExt := filepath.Ext(filePath)
fileID := strings.Replace(fileName, fileExt, "", -1)
if fileID != objectID {
err := fmt.Errorf("kibana object file [%s] defines non-matching ID [%s]", filePath, objectID)
errs = append(errs, err)
}
}

return errs
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package semantic

import "github.com/elastic/package-spec/code/go/internal/errors"

// ValidateKibanaNoDanglingObjectIDs returns validation errors if there are any
// dangling references to Kibana objects in any Kibana object files. That is, it
// returns validation errors if a Kibana object file in the package references another
// Kibana object with ID i, but no Kibana object file for object ID i is found in the
// package.
func ValidateKibanaNoDanglingObjectIDs(pkgRoot string) errors.ValidationErrors {
// TODO: will be implemented in follow up PR
return nil
}
31 changes: 28 additions & 3 deletions code/go/internal/validator/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import (
"path"
"strconv"

ve "github.com/elastic/package-spec/code/go/internal/errors"

"github.com/elastic/package-spec/code/go/internal/validator/semantic"

"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"
"github.com/rakyll/statik/fs"
Expand All @@ -20,6 +24,8 @@ type Spec struct {
specPath string
}

type validationRules []func(pkgRoot string) ve.ValidationErrors

// NewSpec creates a new Spec for the given version
func NewSpec(version semver.Version) (*Spec, error) {
majorVersion := strconv.FormatUint(version.Major(), 10)
Expand All @@ -44,8 +50,8 @@ func NewSpec(version semver.Version) (*Spec, error) {
}

// ValidatePackage validates the given Package against the Spec
func (s Spec) ValidatePackage(pkg Package) ValidationErrors {
var errs ValidationErrors
func (s Spec) ValidatePackage(pkg Package) ve.ValidationErrors {
var errs ve.ValidationErrors

rootSpecPath := path.Join(s.specPath, "spec.yml")
rootSpec, err := newFolderSpec(s.fs, rootSpecPath)
Expand All @@ -54,5 +60,24 @@ func (s Spec) ValidatePackage(pkg Package) ValidationErrors {
return errs
}

return rootSpec.validate(pkg.Name, pkg.RootPath)
// Syntactic validations
errs.Append(rootSpec.validate(pkg.Name, pkg.RootPath))

// Semantic validations
rules := validationRules{
semantic.ValidateKibanaObjectIDs,
}
errs.Append(rules.validate(pkg.RootPath))

return errs
}

func (vr validationRules) validate(pkgRoot string) ve.ValidationErrors {
var errs ve.ValidationErrors
for _, validationRule := range vr {
err := validationRule(pkgRoot)
errs.Append(err)
}

return errs
}
4 changes: 2 additions & 2 deletions code/go/pkg/validator/errors.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package validator

import (
"github.com/elastic/package-spec/code/go/internal/validator"
"github.com/elastic/package-spec/code/go/internal/errors"
)

// ValidationErrors is an Error that contains a iterable collection of validation error messages.
type ValidationErrors validator.ValidationErrors
type ValidationErrors errors.ValidationErrors
Loading

0 comments on commit 92ee6e4

Please sign in to comment.