diff --git a/code/go/internal/fspath/fspath.go b/code/go/internal/fspath/fspath.go new file mode 100644 index 000000000..de4360f73 --- /dev/null +++ b/code/go/internal/fspath/fspath.go @@ -0,0 +1,38 @@ +// 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 fspath + +import ( + "io/fs" + "os" + "path/filepath" +) + +// FS implements the fs interface and can also show a path where the fs is located. +// This is useful to report error messages relative to the location of the file system. +type FS interface { + fs.FS + + Path(name ...string) string +} + +type fsDir struct { + fs.FS + + path string +} + +// Path returns a path for the given names, based on the location of the file system. +func (fs *fsDir) Path(names ...string) string { + return filepath.Join(append([]string{fs.path}, names...)...) +} + +// DirFS returns a file system for a directory, it keeps the path to implement the FS interface. +func DirFS(path string) FS { + return &fsDir{ + FS: os.DirFS(path), + path: path, + } +} diff --git a/code/go/internal/pkgpath/files.go b/code/go/internal/pkgpath/files.go index 82f5b2379..82298e44c 100644 --- a/code/go/internal/pkgpath/files.go +++ b/code/go/internal/pkgpath/files.go @@ -7,7 +7,7 @@ package pkgpath import ( "encoding/json" "fmt" - "io/ioutil" + "io/fs" "os" "path/filepath" "strings" @@ -16,17 +16,20 @@ import ( "github.com/joeshaw/multierror" "github.com/pkg/errors" "gopkg.in/yaml.v3" + + "github.com/elastic/package-spec/code/go/internal/fspath" ) // File represents a file in the package. type File struct { + fsys fspath.FS path string os.FileInfo } // Files finds files for the given glob -func Files(glob string) ([]File, error) { - paths, err := filepath.Glob(glob) +func Files(fsys fspath.FS, glob string) ([]File, error) { + paths, err := fs.Glob(fsys, glob) if err != nil { return nil, err } @@ -34,13 +37,13 @@ func Files(glob string) ([]File, error) { var errs multierror.Errors var files = make([]File, 0) for _, path := range paths { - info, err := os.Stat(path) + info, err := fs.Stat(fsys, path) if err != nil { errs = append(errs, err) continue } - file := File{path, info} + file := File{fsys, path, info} files = append(files, file) } @@ -58,7 +61,7 @@ func (f File) Values(path string) (interface{}, error) { return nil, fmt.Errorf("cannot extract values from file type = %s", fileExt) } - contents, err := ioutil.ReadFile(f.path) + contents, err := fs.ReadFile(f.fsys, f.path) if err != nil { return nil, errors.Wrap(err, "reading file content failed") } @@ -66,11 +69,11 @@ func (f File) Values(path string) (interface{}, error) { 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) + return nil, errors.Wrapf(err, "unmarshalling YAML file failed (path: %s)", f.fsys.Path(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 nil, errors.Wrapf(err, "unmarshalling JSON file failed (path: %s)", f.fsys.Path(fileName)) } } diff --git a/code/go/internal/validator/folder_item_content.go b/code/go/internal/validator/folder_item_content.go index 3b36a537f..c059337b3 100644 --- a/code/go/internal/validator/folder_item_content.go +++ b/code/go/internal/validator/folder_item_content.go @@ -8,15 +8,15 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io/fs" "mime" "github.com/pkg/errors" "gopkg.in/yaml.v3" ) -func loadItemContent(itemPath, mediaType string) ([]byte, error) { - itemData, err := ioutil.ReadFile(itemPath) +func loadItemContent(fsys fs.FS, itemPath, mediaType string) ([]byte, error) { + itemData, err := fs.ReadFile(fsys, itemPath) if err != nil { return nil, errors.Wrap(err, "reading item file failed") } diff --git a/code/go/internal/validator/folder_item_spec.go b/code/go/internal/validator/folder_item_spec.go index 8da61211d..2455f0489 100644 --- a/code/go/internal/validator/folder_item_spec.go +++ b/code/go/internal/validator/folder_item_spec.go @@ -7,7 +7,6 @@ package validator import ( "fmt" "io/fs" - "os" "path/filepath" "regexp" "sync" @@ -35,7 +34,7 @@ type folderItemSpec struct { var formatCheckersMutex sync.Mutex -func (s *folderItemSpec) matchingFileExists(files []os.FileInfo) (bool, error) { +func (s *folderItemSpec) matchingFileExists(files []fs.DirEntry) (bool, error) { if s.Name != "" { for _, file := range files { if file.Name() == s.Name { @@ -57,7 +56,13 @@ func (s *folderItemSpec) matchingFileExists(files []os.FileInfo) (bool, error) { return false, nil } -func (s *folderItemSpec) isSameType(file os.FileInfo) bool { +// sameFileChecker is the interface that parameters of isSameType should implement, +// this is intended to accept both fs.DirEntry and fs.FileInfo. +type sameFileChecker interface { + IsDir() bool +} + +func (s *folderItemSpec) isSameType(file sameFileChecker) bool { switch s.ItemType { case itemTypeFile: return !file.IsDir() @@ -68,9 +73,9 @@ func (s *folderItemSpec) isSameType(file os.FileInfo) bool { return false } -func (s *folderItemSpec) validate(fs fs.FS, folderSpecPath string, itemPath string) ve.ValidationErrors { +func (s *folderItemSpec) validate(schemaFS fs.FS, fsys fs.FS, folderSpecPath string, itemPath string) ve.ValidationErrors { // loading item content - itemData, err := loadItemContent(itemPath, s.ContentMediaType) + itemData, err := loadItemContent(fsys, itemPath, s.ContentMediaType) if err != nil { return ve.ValidationErrors{err} } @@ -78,7 +83,7 @@ func (s *folderItemSpec) validate(fs fs.FS, folderSpecPath string, itemPath stri var schemaLoader gojsonschema.JSONLoader if s.Ref != "" { schemaPath := filepath.Join(filepath.Dir(folderSpecPath), s.Ref) - schemaLoader = yamlschema.NewReferenceLoaderFileSystem("file:///"+schemaPath, fs) + schemaLoader = yamlschema.NewReferenceLoaderFileSystem("file:///"+schemaPath, schemaFS) } else { return nil // item's schema is not defined } @@ -93,8 +98,8 @@ func (s *folderItemSpec) validate(fs fs.FS, folderSpecPath string, itemPath stri formatCheckersMutex.Unlock() }() - semantic.LoadRelativePathFormatChecker(filepath.Dir(itemPath)) - semantic.LoadDataStreamNameFormatChecker(filepath.Dir(itemPath)) + semantic.LoadRelativePathFormatChecker(fsys, filepath.Dir(itemPath)) + semantic.LoadDataStreamNameFormatChecker(fsys, filepath.Dir(itemPath)) result, err := gojsonschema.Validate(schemaLoader, documentLoader) if err != nil { return ve.ValidationErrors{err} diff --git a/code/go/internal/validator/folder_item_spec_format.go b/code/go/internal/validator/folder_item_spec_format.go deleted file mode 100644 index 451a0b471..000000000 --- a/code/go/internal/validator/folder_item_spec_format.go +++ /dev/null @@ -1,30 +0,0 @@ -// 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 validator - -import ( - "os" - "path/filepath" -) - -// RelativePathChecker is responsible for checking presence of the file path -type RelativePathChecker struct { - currentPath string -} - -// IsFormat method checks if the path exists. -func (r RelativePathChecker) IsFormat(input interface{}) bool { - asString, ok := input.(string) - if !ok { - return false - } - - path := filepath.Join(r.currentPath, asString) - _, err := os.Stat(path) - if err != nil { - return false - } - return true -} \ No newline at end of file diff --git a/code/go/internal/validator/folder_spec.go b/code/go/internal/validator/folder_spec.go index e1fe5ba25..5bb98f31c 100644 --- a/code/go/internal/validator/folder_spec.go +++ b/code/go/internal/validator/folder_spec.go @@ -8,15 +8,15 @@ import ( "fmt" "io/fs" "io/ioutil" - "path" "path/filepath" "regexp" "strings" - ve "github.com/elastic/package-spec/code/go/internal/errors" - "github.com/pkg/errors" "gopkg.in/yaml.v3" + + ve "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/fspath" ) const ( @@ -66,11 +66,11 @@ func newFolderSpec(fs fs.FS, specPath string) (*folderSpec, error) { return &spec, nil } -func (s *folderSpec) validate(packageName string, folderPath string) ve.ValidationErrors { +func (s *folderSpec) validate(packageName string, fsys fspath.FS, path string) ve.ValidationErrors { var errs ve.ValidationErrors - files, err := ioutil.ReadDir(folderPath) + files, err := fs.ReadDir(fsys, path) if err != nil { - errs = append(errs, errors.Wrapf(err, "could not read folder [%s]", folderPath)) + errs = append(errs, errors.Wrapf(err, "could not read folder [%s]", fsys.Path(path))) return errs } @@ -87,8 +87,8 @@ func (s *folderSpec) validate(packageName string, folderPath string) ve.Validati if file.IsDir() { if !s.DevelopmentFolder && strings.Contains(fileName, "-") { errs = append(errs, - fmt.Errorf(`file "%s/%s" is invalid: directory name inside package %s contains -: %s`, - folderPath, fileName, packageName, fileName)) + fmt.Errorf(`file "%s" is invalid: directory name inside package %s contains -: %s`, + fsys.Path(path, fileName), packageName, fileName)) } } continue @@ -96,7 +96,7 @@ func (s *folderSpec) validate(packageName string, folderPath string) ve.Validati if itemSpec == nil && !s.AdditionalContents { // No spec found for current folder item and we do not allow additional contents in folder. - errs = append(errs, fmt.Errorf("item [%s] is not allowed in folder [%s]", fileName, folderPath)) + errs = append(errs, fmt.Errorf("item [%s] is not allowed in folder [%s]", fileName, fsys.Path(path))) continue } @@ -118,7 +118,7 @@ func (s *folderSpec) validate(packageName string, folderPath string) ve.Validati var subFolderSpec *folderSpec if itemSpec.Ref != "" { - subFolderSpecPath := path.Join(filepath.Dir(s.specPath), itemSpec.Ref) + subFolderSpecPath := filepath.Join(filepath.Dir(s.specPath), itemSpec.Ref) subFolderSpec, err = newFolderSpec(s.fs, subFolderSpecPath) if err != nil { errs = append(errs, err) @@ -140,23 +140,23 @@ func (s *folderSpec) validate(packageName string, folderPath string) ve.Validati subFolderSpec.DevelopmentFolder = true } - subFolderPath := path.Join(folderPath, fileName) - subErrs := subFolderSpec.validate(packageName, subFolderPath) + subFolderPath := filepath.Join(path, fileName) + subErrs := subFolderSpec.validate(packageName, fsys, subFolderPath) if len(subErrs) > 0 { errs = append(errs, subErrs...) } } else { if !itemSpec.isSameType(file) { - errs = append(errs, fmt.Errorf("[%s] is a file but is expected to be a folder", fileName)) + errs = append(errs, fmt.Errorf("[%s] is a file but is expected to be a folder", fsys.Path(fileName))) continue } - itemPath := filepath.Join(folderPath, file.Name()) - itemValidationErrs := itemSpec.validate(s.fs, s.specPath, itemPath) + itemPath := filepath.Join(path, file.Name()) + itemValidationErrs := itemSpec.validate(s.fs, fsys, s.specPath, itemPath) if itemValidationErrs != nil { for _, ive := range itemValidationErrs { - errs = append(errs, errors.Wrapf(ive, "file \"%s\" is invalid", itemPath)) + errs = append(errs, errors.Wrapf(ive, "file \"%s\" is invalid", fsys.Path(itemPath))) } } } @@ -177,9 +177,9 @@ func (s *folderSpec) validate(packageName string, folderPath string) ve.Validati if !fileFound { var err error if itemSpec.Name != "" { - err = fmt.Errorf("expecting to find [%s] %s in folder [%s]", itemSpec.Name, itemSpec.ItemType, folderPath) + err = fmt.Errorf("expecting to find [%s] %s in folder [%s]", itemSpec.Name, itemSpec.ItemType, fsys.Path(path)) } else if itemSpec.Pattern != "" { - err = fmt.Errorf("expecting to find %s matching pattern [%s] in folder [%s]", itemSpec.ItemType, itemSpec.Pattern, folderPath) + err = fmt.Errorf("expecting to find %s matching pattern [%s] in folder [%s]", itemSpec.ItemType, itemSpec.Pattern, fsys.Path(path)) } errs = append(errs, err) } diff --git a/code/go/internal/validator/package.go b/code/go/internal/validator/package.go index a7fedd029..9b9ce49ca 100644 --- a/code/go/internal/validator/package.go +++ b/code/go/internal/validator/package.go @@ -6,9 +6,9 @@ package validator import ( "fmt" - "io/ioutil" + "io/fs" "os" - "path" + "path/filepath" "github.com/Masterminds/semver/v3" "github.com/pkg/errors" @@ -19,7 +19,19 @@ import ( type Package struct { Name string SpecVersion *semver.Version - RootPath string + + fs fs.FS + location string +} + +// Open opens a file in the package filesystem. +func (p *Package) Open(name string) (fs.File, error) { + return p.fs.Open(name) +} + +// Path returns a path meaningful for the user. +func (p *Package) Path(names ...string) string { + return filepath.Join(append([]string{p.location}, names...)...) } // NewPackage creates a new Package from a path to the package's root folder @@ -33,13 +45,19 @@ func NewPackage(pkgRootPath string) (*Package, error) { return nil, fmt.Errorf("no package folder found at path [%v]", pkgRootPath) } - pkgManifestPath := path.Join(pkgRootPath, "manifest.yml") - info, err = os.Stat(pkgManifestPath) + return NewPackageFromFS(pkgRootPath, os.DirFS(pkgRootPath)) +} + +// NewPackageFromFS creates a new package from a given filesystem. A root path can be indicated +// to help building paths meaningful for the users. +func NewPackageFromFS(location string, fsys fs.FS) (*Package, error) { + pkgManifestPath := "manifest.yml" + _, err := fs.Stat(fsys, pkgManifestPath) if os.IsNotExist(err) { return nil, errors.Wrapf(err, "no package manifest file found at path [%v]", pkgManifestPath) } - data, err := ioutil.ReadFile(pkgManifestPath) + data, err := fs.ReadFile(fsys, pkgManifestPath) if err != nil { return nil, fmt.Errorf("could not read package manifest file [%v]", pkgManifestPath) } @@ -60,8 +78,10 @@ func NewPackage(pkgRootPath string) (*Package, error) { // Instantiate Package object and return it p := Package{ Name: manifest.Name, - RootPath: pkgRootPath, SpecVersion: specVersion, + fs: fsys, + + location: location, } return &p, nil diff --git a/code/go/internal/validator/package_test.go b/code/go/internal/validator/package_test.go index 6801f7102..9c56b334b 100644 --- a/code/go/internal/validator/package_test.go +++ b/code/go/internal/validator/package_test.go @@ -38,7 +38,7 @@ func TestNewPackage(t *testing.T) { if test.expectedErrContains == "" { require.NoError(t, err) require.Equal(t, test.expectedSpecVersion, pkg.SpecVersion) - require.Equal(t, pkgRootPath, pkg.RootPath) + require.Equal(t, pkgRootPath, pkg.Path()) } else { require.Error(t, err) require.Contains(t, err.Error(), test.expectedErrContains) diff --git a/code/go/internal/validator/semantic/format_checkers.go b/code/go/internal/validator/semantic/format_checkers.go index 083a35c32..4280fc823 100644 --- a/code/go/internal/validator/semantic/format_checkers.go +++ b/code/go/internal/validator/semantic/format_checkers.go @@ -5,7 +5,7 @@ package semantic import ( - "os" + "io/fs" "path/filepath" "github.com/xeipuuv/gojsonschema" @@ -26,6 +26,7 @@ const ( // relativePathChecker is responsible for checking presence of the file path type relativePathChecker struct { + fsys fs.FS currentPath string } @@ -37,7 +38,7 @@ func (r relativePathChecker) IsFormat(input interface{}) bool { } path := filepath.Join(r.currentPath, asString) - _, err := os.Stat(path) + _, err := fs.Stat(r.fsys, path) if err != nil { return false } @@ -46,8 +47,9 @@ func (r relativePathChecker) IsFormat(input interface{}) bool { // LoadRelativePathFormatChecker loads the relative-path format checker into the // json-schema validation library. -func LoadRelativePathFormatChecker(currentPath string) { +func LoadRelativePathFormatChecker(fsys fs.FS, currentPath string) { gojsonschema.FormatCheckers.Add(RelativePathFormat, relativePathChecker{ + fsys: fsys, currentPath: currentPath, }) } @@ -60,8 +62,9 @@ func UnloadRelativePathFormatChecker() { // LoadDataStreamNameFormatChecker loads the data-stream-name format checker into the // json-schema validation library. -func LoadDataStreamNameFormatChecker(currentPath string) { +func LoadDataStreamNameFormatChecker(fsys fs.FS, currentPath string) { gojsonschema.FormatCheckers.Add(DataStreamNameFormat, relativePathChecker{ + fsys: fsys, currentPath: filepath.Join(currentPath, "data_stream"), }) } diff --git a/code/go/internal/validator/semantic/kibana_matching_object_ids.go b/code/go/internal/validator/semantic/kibana_matching_object_ids.go index f561b1a28..b99ff7682 100644 --- a/code/go/internal/validator/semantic/kibana_matching_object_ids.go +++ b/code/go/internal/validator/semantic/kibana_matching_object_ids.go @@ -9,21 +9,22 @@ import ( "path/filepath" "strings" - ve "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/pkg/errors" + ve "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/fspath" "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 { +func ValidateKibanaObjectIDs(fsys fspath.FS) ve.ValidationErrors { var errs ve.ValidationErrors - filePaths := filepath.Join(pkgRoot, "kibana", "*", "*.json") - objectFiles, err := pkgpath.Files(filePaths) + filePaths := filepath.Join("kibana", "*", "*.json") + objectFiles, err := pkgpath.Files(fsys, filePaths) if err != nil { errs = append(errs, errors.Wrap(err, "error finding Kibana object files")) return errs @@ -34,7 +35,7 @@ func ValidateKibanaObjectIDs(pkgRoot string) ve.ValidationErrors { objectID, err := objectFile.Values("$.id") if err != nil { - errs = append(errs, errors.Wrapf(err, "unable to get Kibana object ID in file [%s]", filePath)) + errs = append(errs, errors.Wrapf(err, "unable to get Kibana object ID in file [%s]", fsys.Path(filePath))) continue } @@ -42,7 +43,7 @@ func ValidateKibanaObjectIDs(pkgRoot string) ve.ValidationErrors { if filepath.Base(filepath.Dir(filePath)) == "security_rule" { ruleID, err := objectFile.Values("$.attributes.rule_id") if err != nil { - errs = append(errs, errors.Wrapf(err, "unable to get rule ID in file [%s]", filePath)) + errs = append(errs, errors.Wrapf(err, "unable to get rule ID in file [%s]", fsys.Path(filePath))) continue } @@ -57,7 +58,7 @@ func ValidateKibanaObjectIDs(pkgRoot string) ve.ValidationErrors { 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) + err := fmt.Errorf("kibana object file [%s] defines non-matching ID [%s]", fsys.Path(filePath), objectID) errs = append(errs, err) } } diff --git a/code/go/internal/validator/semantic/types.go b/code/go/internal/validator/semantic/types.go index 4aacf4de9..0ebcdd31c 100644 --- a/code/go/internal/validator/semantic/types.go +++ b/code/go/internal/validator/semantic/types.go @@ -5,14 +5,15 @@ package semantic import ( - "gopkg.in/yaml.v3" - "io/ioutil" + "io/fs" "os" "path/filepath" "github.com/pkg/errors" + "gopkg.in/yaml.v3" ve "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/fspath" ) type fields []field @@ -30,20 +31,20 @@ type field struct { type validateFunc func(fieldsFile string, f field) ve.ValidationErrors -func validateFields(pkgRoot string, validate validateFunc) ve.ValidationErrors { - fieldsFiles, err := listFieldsFiles(pkgRoot) +func validateFields(fsys fspath.FS, validate validateFunc) ve.ValidationErrors { + fieldsFiles, err := listFieldsFiles(fsys) if err != nil { return ve.ValidationErrors{errors.Wrap(err, "can't list fields files")} } var vErrs ve.ValidationErrors for _, fieldsFile := range fieldsFiles { - unmarshaled, err := unmarshalFields(fieldsFile) + unmarshaled, err := unmarshalFields(fsys, fieldsFile) if err != nil { - vErrs = append(vErrs, errors.Wrapf(err, `file "%s" is invalid: can't unmarshal fields`, fieldsFile)) + vErrs = append(vErrs, errors.Wrapf(err, `file "%s" is invalid: can't unmarshal fields`, fsys.Path(fieldsFile))) } - errs := validateNestedFields("", fieldsFile, unmarshaled, validate) + errs := validateNestedFields("", fsys.Path(fieldsFile), unmarshaled, validate) if len(errs) > 0 { vErrs = append(vErrs, errs...) } @@ -71,11 +72,11 @@ func validateNestedFields(parent string, fieldsFile string, fields fields, valid return result } -func listFieldsFiles(pkgRoot string) ([]string, error) { +func listFieldsFiles(fsys fspath.FS) ([]string, error) { var fieldsFiles []string - dataStreamDir := filepath.Join(pkgRoot, "data_stream") - dataStreams, err := ioutil.ReadDir(dataStreamDir) + dataStreamDir := "data_stream" + dataStreams, err := fs.ReadDir(fsys, dataStreamDir) if errors.Is(err, os.ErrNotExist) { return fieldsFiles, nil } @@ -85,12 +86,12 @@ func listFieldsFiles(pkgRoot string) ([]string, error) { for _, dataStream := range dataStreams { fieldsDir := filepath.Join(dataStreamDir, dataStream.Name(), "fields") - fs, err := ioutil.ReadDir(fieldsDir) + fs, err := fs.ReadDir(fsys, fieldsDir) if errors.Is(err, os.ErrNotExist) { continue } if err != nil { - return nil, errors.Wrapf(err, "can't list fields directory (path: %s)", fieldsDir) + return nil, errors.Wrapf(err, "can't list fields directory (path: %s)", fsys.Path(fieldsDir)) } for _, f := range fs { @@ -101,8 +102,8 @@ func listFieldsFiles(pkgRoot string) ([]string, error) { return fieldsFiles, nil } -func unmarshalFields(fieldsPath string) (fields, error) { - content, err := ioutil.ReadFile(fieldsPath) +func unmarshalFields(fsys fspath.FS, fieldsPath string) (fields, error) { + content, err := fs.ReadFile(fsys, fieldsPath) if err != nil { return nil, errors.Wrapf(err, "can't read file (path: %s)", fieldsPath) } diff --git a/code/go/internal/validator/semantic/validate_dimensions.go b/code/go/internal/validator/semantic/validate_dimensions.go index 6306d3c35..f7a6348d7 100644 --- a/code/go/internal/validator/semantic/validate_dimensions.go +++ b/code/go/internal/validator/semantic/validate_dimensions.go @@ -9,11 +9,12 @@ import ( "strings" "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/fspath" ) // ValidateDimensionFields verifies if dimension fields are of one of the expected types. -func ValidateDimensionFields(pkgRoot string) errors.ValidationErrors { - return validateFields(pkgRoot, validateDimensionField) +func ValidateDimensionFields(fsys fspath.FS) errors.ValidationErrors { + return validateFields(fsys, validateDimensionField) } func validateDimensionField(fieldsFile string, f field) errors.ValidationErrors { diff --git a/code/go/internal/validator/semantic/validate_field_groups.go b/code/go/internal/validator/semantic/validate_field_groups.go index 52ca8df18..87d21c980 100644 --- a/code/go/internal/validator/semantic/validate_field_groups.go +++ b/code/go/internal/validator/semantic/validate_field_groups.go @@ -8,11 +8,12 @@ import ( "fmt" "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/fspath" ) // ValidateFieldGroups verifies if field groups don't have units and metric types defined. -func ValidateFieldGroups(pkgRoot string) errors.ValidationErrors { - return validateFields(pkgRoot, validateFieldUnit) +func ValidateFieldGroups(fsys fspath.FS) errors.ValidationErrors { + return validateFields(fsys, validateFieldUnit) } func validateFieldUnit(fieldsFile string, f field) errors.ValidationErrors { diff --git a/code/go/internal/validator/semantic/validate_field_groups_test.go b/code/go/internal/validator/semantic/validate_field_groups_test.go index bc71f08cd..cbc667cfc 100644 --- a/code/go/internal/validator/semantic/validate_field_groups_test.go +++ b/code/go/internal/validator/semantic/validate_field_groups_test.go @@ -11,12 +11,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/elastic/package-spec/code/go/internal/fspath" ) func TestValidateFieldGroups_Good(t *testing.T) { pkgRoot := "../../../../../test/packages/good" - errs := ValidateFieldGroups(pkgRoot) + errs := ValidateFieldGroups(fspath.DirFS(pkgRoot)) require.Empty(t, errs) } @@ -29,7 +31,7 @@ func TestValidateFieldGroups_Bad(t *testing.T) { expected) } - errs := ValidateFieldGroups(pkgRoot) + errs := ValidateFieldGroups(fspath.DirFS(pkgRoot)) if assert.Len(t, errs, 3) { assert.Equal(t, fileError("data_stream/bar/fields/hello-world.yml", `field "aaa.bbb" can't have unit property'`), errs[0].Error()) assert.Equal(t, fileError("data_stream/bar/fields/hello-world.yml", `field "ddd.eee" can't have unit property'`), errs[1].Error()) diff --git a/code/go/internal/validator/semantic/validate_prerelease.go b/code/go/internal/validator/semantic/validate_prerelease.go index c2eff26e8..499290410 100644 --- a/code/go/internal/validator/semantic/validate_prerelease.go +++ b/code/go/internal/validator/semantic/validate_prerelease.go @@ -12,6 +12,7 @@ import ( "github.com/Masterminds/semver/v3" ve "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/fspath" ) var ( @@ -31,8 +32,8 @@ var ( ) // ValidatePrerelease validates additional restrictions on the prerelease tags. -func ValidatePrerelease(pkgRoot string) ve.ValidationErrors { - manifestVersion, err := readManifestVersion(pkgRoot) +func ValidatePrerelease(fsys fspath.FS) ve.ValidationErrors { + manifestVersion, err := readManifestVersion(fsys) if err != nil { return ve.ValidationErrors{err} } diff --git a/code/go/internal/validator/semantic/version_integrity.go b/code/go/internal/validator/semantic/version_integrity.go index 087d61c2d..0cdbc4896 100644 --- a/code/go/internal/validator/semantic/version_integrity.go +++ b/code/go/internal/validator/semantic/version_integrity.go @@ -6,24 +6,24 @@ package semantic import ( "fmt" - "path/filepath" "strings" "github.com/pkg/errors" ve "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/fspath" "github.com/elastic/package-spec/code/go/internal/pkgpath" ) // ValidateVersionIntegrity returns validation errors if the version defined in manifest isn't referenced in the latest // entry of the changelog file. -func ValidateVersionIntegrity(pkgRoot string) ve.ValidationErrors { - manifestVersion, err := readManifestVersion(pkgRoot) +func ValidateVersionIntegrity(fsys fspath.FS) ve.ValidationErrors { + manifestVersion, err := readManifestVersion(fsys) if err != nil { return ve.ValidationErrors{err} } - changelogVersions, err := readChangelogVersions(pkgRoot) + changelogVersions, err := readChangelogVersions(fsys) if err != nil { return ve.ValidationErrors{err} } @@ -40,9 +40,9 @@ func ValidateVersionIntegrity(pkgRoot string) ve.ValidationErrors { return nil } -func readManifestVersion(pkgRoot string) (string, error) { - manifestPath := filepath.Join(pkgRoot, "manifest.yml") - f, err := pkgpath.Files(manifestPath) +func readManifestVersion(fsys fspath.FS) (string, error) { + manifestPath := "manifest.yml" + f, err := pkgpath.Files(fsys, manifestPath) if err != nil { return "", errors.Wrap(err, "can't locate manifest file") } @@ -63,9 +63,9 @@ func readManifestVersion(pkgRoot string) (string, error) { return sVal, nil } -func readChangelogVersions(pkgRoot string) ([]string, error) { - manifestPath := filepath.Join(pkgRoot, "changelog.yml") - f, err := pkgpath.Files(manifestPath) +func readChangelogVersions(fsys fspath.FS) ([]string, error) { + changelogPath := "changelog.yml" + f, err := pkgpath.Files(fsys, changelogPath) if err != nil { return nil, errors.Wrap(err, "can't locate changelog file") } diff --git a/code/go/internal/validator/spec.go b/code/go/internal/validator/spec.go index 08d524e3d..c8b57c434 100644 --- a/code/go/internal/validator/spec.go +++ b/code/go/internal/validator/spec.go @@ -9,13 +9,13 @@ import ( "path" "strconv" - ve "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/Masterminds/semver/v3" + "github.com/pkg/errors" spec "github.com/elastic/package-spec" + ve "github.com/elastic/package-spec/code/go/internal/errors" + "github.com/elastic/package-spec/code/go/internal/fspath" "github.com/elastic/package-spec/code/go/internal/validator/semantic" - - "github.com/Masterminds/semver/v3" - "github.com/pkg/errors" ) // Spec represents a package specification @@ -25,7 +25,7 @@ type Spec struct { specPath string } -type validationRules []func(pkgRoot string) ve.ValidationErrors +type validationRules []func(pkg fspath.FS) ve.ValidationErrors // NewSpec creates a new Spec for the given version func NewSpec(version semver.Version) (*Spec, error) { @@ -58,7 +58,7 @@ func (s Spec) ValidatePackage(pkg Package) ve.ValidationErrors { } // Syntactic validations - errs = rootSpec.validate(pkg.Name, pkg.RootPath) + errs = rootSpec.validate(pkg.Name, &pkg, ".") if len(errs) != 0 { return errs } @@ -71,13 +71,14 @@ func (s Spec) ValidatePackage(pkg Package) ve.ValidationErrors { semantic.ValidateFieldGroups, semantic.ValidateDimensionFields, } - return rules.validate(pkg.RootPath) + + return rules.validate(&pkg) } -func (vr validationRules) validate(pkgRoot string) ve.ValidationErrors { +func (vr validationRules) validate(fsys fspath.FS) ve.ValidationErrors { var errs ve.ValidationErrors for _, validationRule := range vr { - err := validationRule(pkgRoot) + err := validationRule(fsys) errs.Append(err) } diff --git a/code/go/pkg/validator/validator.go b/code/go/pkg/validator/validator.go index ab7eb438b..bd29681e0 100644 --- a/code/go/pkg/validator/validator.go +++ b/code/go/pkg/validator/validator.go @@ -5,7 +5,11 @@ package validator import ( + "archive/zip" "errors" + "fmt" + "io/fs" + "os" "github.com/elastic/package-spec/code/go/internal/validator" ) @@ -13,7 +17,37 @@ import ( // ValidateFromPath validates a package located at the given path against the // appropriate specification and returns any errors. func ValidateFromPath(packageRootPath string) error { - pkg, err := validator.NewPackage(packageRootPath) + return ValidateFromFS(packageRootPath, os.DirFS(packageRootPath)) +} + +// ValidateFromZip validates a package on its zip format. +func ValidateFromZip(packagePath string) error { + r, err := zip.OpenReader(packagePath) + if err != nil { + return fmt.Errorf("failed to open zip file (%s): %w", packagePath, err) + } + defer r.Close() + + dirs, err := fs.ReadDir(r, ".") + if err != nil { + return fmt.Errorf("failed to read root directory in zip file (%s): %w", packagePath, err) + } + if len(dirs) != 1 { + return fmt.Errorf("a single directory is expected in zip file, %d found", len(dirs)) + } + + subDir, err := fs.Sub(r, dirs[0].Name()) + if err != nil { + return err + } + + return ValidateFromFS(packagePath, subDir) +} + +// ValidateFromFS validates a package against the appropiate specification and returns any errors. +// Package files are obtained throug the given filesystem. +func ValidateFromFS(location string, fsys fs.FS) error { + pkg, err := validator.NewPackageFromFS(location, fsys) if err != nil { return err }