Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Semantic validations #160

Merged
merged 18 commits into from
Apr 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
mtojek marked this conversation as resolved.
Show resolved Hide resolved
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
ycombinator marked this conversation as resolved.
Show resolved Hide resolved
Loading