Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// 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 (
"encoding/json"
"errors"
"fmt"
"io/fs"
"path"
"slices"

"gopkg.in/yaml.v3"

"github.com/elastic/package-spec/v3/code/go/internal/fspath"
"github.com/elastic/package-spec/v3/code/go/pkg/specerrors"
)

const (
tagType = "tag"
)

type packageSpecTag struct {
Attributes struct {
Name string `yaml:"name"`
} `yaml:"attributes"`
Type string `yaml:"type"`
}

type sharedTagYML struct {
Name string `yaml:"text"`
}

// ValidateKibanaTagDuplicates checks for duplicate Kibana tag names
// between the kibana/tags.yml file and the tags defined in the package's kibana/tag/*.json files.
// It returns a list of validation errors if any duplicates are found.
func ValidateKibanaTagDuplicates(fsys fspath.FS) specerrors.ValidationErrors {
var errs specerrors.ValidationErrors
sharedTagNames, verr := getValidatedSharedKibanaTags(fsys)
if len(verr) > 0 {
errs = append(errs, verr...)
}

verr = validateKibanaPackageTagsDuplicates(fsys, sharedTagNames)
if len(verr) > 0 {
errs = append(errs, verr...)
}
return errs
}

// getValidatedSharedKibanaTags reads the kibana/tags.yml file and returns a slice of tag names defined in it.
// It also returns any validation errors encountered during the process if tags are duplicated within the file.
func getValidatedSharedKibanaTags(fsys fspath.FS) ([]string, specerrors.ValidationErrors) {
tagsPath := path.Join("kibana", "tags.yml")
// Collect all tags defined in the kibana/tags.yml file.
b, err := fs.ReadFile(fsys, tagsPath)
if err != nil {
// if the file does not exist, return an empty slice without error
if errors.Is(err, fs.ErrNotExist) {
return nil, nil
}
return nil, specerrors.ValidationErrors{specerrors.NewStructuredErrorf("error reading file %s: %v", tagsPath, err)}
}
var sharedKibanaTags []sharedTagYML
err = yaml.Unmarshal(b, &sharedKibanaTags)
if err != nil {
return nil, specerrors.ValidationErrors{specerrors.NewStructuredErrorf("error unmarshaling file %s: %v", tagsPath, err)}
}

tags := make([]string, 0)
errs := make(specerrors.ValidationErrors, 0)
// Check for duplicate tag names in the kibana/tags.yml file.
for _, tag := range sharedKibanaTags {
if slices.Contains(tags, tag.Name) {
errs = append(errs, specerrors.NewStructuredError(
fmt.Errorf("file \"%s\" is invalid: duplicate tag name '%s' found", fsys.Path(tagsPath), tag.Name), specerrors.CodeKibanaTagDuplicates))
continue
}
tags = append(tags, tag.Name)
}
return tags, errs
}

func validateKibanaPackageTagsDuplicates(fsys fspath.FS, sharedTagNames []string) specerrors.ValidationErrors {
entries, err := fs.ReadDir(fsys, path.Join("kibana", "tag"))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
}
return specerrors.ValidationErrors{specerrors.NewStructuredErrorf("error reading kibana/tag directory: %v", err)}
}

tags := make([]string, 0)
errs := make(specerrors.ValidationErrors, 0)
for _, entry := range entries {
if entry.IsDir() || path.Ext(entry.Name()) != ".json" {
// skip non-json files and directories
continue
}
filePath := path.Join("kibana", "tag", entry.Name())
b, err := fs.ReadFile(fsys, filePath)
if err != nil {
errs = append(errs, specerrors.NewStructuredErrorf("error reading file %s: %v", fsys.Path(filePath), err))
continue
}
var pkgTag packageSpecTag
err = json.Unmarshal(b, &pkgTag)
if err != nil {
errs = append(errs, specerrors.NewStructuredErrorf("error unmarshaling file %s: %v", fsys.Path(filePath), err))
continue
}
// skip non-tag types
if pkgTag.Type != tagType {
continue
}

// validate if the tag is already defined in other json file
if slices.Contains(tags, pkgTag.Attributes.Name) {
errs = append(errs, specerrors.NewStructuredError(
fmt.Errorf("file \"%s\" is invalid: duplicate package tag name '%s' found", fsys.Path(filePath), pkgTag.Attributes.Name), specerrors.CodeKibanaTagDuplicates))
continue
}
if slices.Contains(sharedTagNames, pkgTag.Attributes.Name) {
errs = append(errs, specerrors.NewStructuredError(
fmt.Errorf("file \"%s\" is invalid: tag name '%s' is already defined in tags.yml", fsys.Path(filePath), pkgTag.Attributes.Name), specerrors.CodeKibanaTagDuplicates))
continue
}
tags = append(tags, pkgTag.Attributes.Name)
}
return errs
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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 (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

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

func TestGetValidatedSharedKibanaTags(t *testing.T) {
t.Run("no tags.yml file", func(t *testing.T) {
tmpDir := t.TempDir()
fsys := fspath.DirFS(tmpDir)

tags, errs := getValidatedSharedKibanaTags(fsys)
require.Empty(t, errs)
assert.Empty(t, tags)
})

t.Run("with tags.yml file and duplicates", func(t *testing.T) {
tmpDir := t.TempDir()
kibanaDir := filepath.Join(tmpDir, "kibana")
err := os.MkdirAll(kibanaDir, 0o755)
require.NoError(t, err)

tagsYMLPath := filepath.Join(kibanaDir, "tags.yml")
tagsYMLContent := `- text: tag1
- text: tag2
- text: tag1
`
err = os.WriteFile(tagsYMLPath, []byte(tagsYMLContent), 0o644)
require.NoError(t, err)

fsys := fspath.DirFS(tmpDir)
tags, errs := getValidatedSharedKibanaTags(fsys)
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "duplicate tag name 'tag1' found (SVR00007)")
require.Len(t, tags, 2)
assert.Contains(t, tags, "tag1")
assert.Contains(t, tags, "tag2")
})
}

func TestValidateKibanaPackageTagsDuplicates(t *testing.T) {
t.Run("with duplicate tags in JSON files", func(t *testing.T) {
tmpDir := t.TempDir()
kibanaTagDir := filepath.Join(tmpDir, "kibana", "tag")
err := os.MkdirAll(kibanaTagDir, 0o755)
require.NoError(t, err)

tag1Path := filepath.Join(kibanaTagDir, "tag1.json")
tag1Content := `{
"attributes": {
"name": "tagA"
},
"type": "tag"
}`
err = os.WriteFile(tag1Path, []byte(tag1Content), 0o644)
require.NoError(t, err)

tag2Path := filepath.Join(kibanaTagDir, "tag2.json")
tag2Content := `{
"attributes": {
"name": "tagA"
},
"type": "tag"
}`
err = os.WriteFile(tag2Path, []byte(tag2Content), 0o644)
require.NoError(t, err)

fsys := fspath.DirFS(tmpDir)
tags := []string{"tagB"}
errs := validateKibanaPackageTagsDuplicates(fsys, tags)
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "duplicate package tag name 'tagA'")
})

t.Run("with tag in JSON already defined in tags.yml", func(t *testing.T) {
tmpDir := t.TempDir()
kibanaTagDir := filepath.Join(tmpDir, "kibana", "tag")
err := os.MkdirAll(kibanaTagDir, 0o755)
require.NoError(t, err)

tag1Path := filepath.Join(kibanaTagDir, "tag1.json")
tag1Content := `{
"attributes": {
"name": "tagB"
},
"type": "tag"
}`
err = os.WriteFile(tag1Path, []byte(tag1Content), 0o644)
require.NoError(t, err)

fsys := fspath.DirFS(tmpDir)
tags := []string{"tagB"}
errs := validateKibanaPackageTagsDuplicates(fsys, tags)
require.Len(t, errs, 1)
assert.Contains(t, errs[0].Error(), "tag name 'tagB' is already defined in tags.yml (SVR00007)")
})

t.Run("with unique tags in JSON files", func(t *testing.T) {
tmpDir := t.TempDir()
kibanaTagDir := filepath.Join(tmpDir, "kibana", "tag")
err := os.MkdirAll(kibanaTagDir, 0o755)
require.NoError(t, err)

tag1Path := filepath.Join(kibanaTagDir, "tag1.json")
tag1Content := `{
"attributes": {
"name": "tagA"
},
"type": "tag"
}`
err = os.WriteFile(tag1Path, []byte(tag1Content), 0o644)
require.NoError(t, err)

tag2Path := filepath.Join(kibanaTagDir, "tag2.json")
tag2Content := `{
"attributes": {
"name": "tagB"
},
"type": "tag"
}`
err = os.WriteFile(tag2Path, []byte(tag2Content), 0o644)
require.NoError(t, err)

fsys := fspath.DirFS(tmpDir)
tags := []string{"tagC"}
errs := validateKibanaPackageTagsDuplicates(fsys, tags)
require.Empty(t, errs)
})
}
1 change: 1 addition & 0 deletions code/go/internal/validator/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ func (s Spec) rules(pkgType string, rootSpec spectypes.ItemSpec) validationRules
{fn: semantic.ValidateIntegrationPolicyTemplates, types: []string{"integration"}},
{fn: semantic.ValidatePipelineTags, types: []string{"integration"}, since: semver.MustParse("3.6.0")},
{fn: semantic.ValidateStaticHandlebarsFiles, types: []string{"integration", "input"}},
{fn: semantic.ValidateKibanaTagDuplicates},
}

var validationRules validationRules
Expand Down
1 change: 1 addition & 0 deletions code/go/pkg/specerrors/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ const (
CodeVisualizationByValue = "SVR00004"
CodeMinimumKibanaVersion = "SVR00005"
CodePipelineTagRequired = "SVR00006"
CodeKibanaTagDuplicates = "SVR00007"
)
8 changes: 8 additions & 0 deletions code/go/pkg/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,14 @@ func TestValidateFile(t *testing.T) {
"elasticsearch/esql_view/view.yml",
[]string{"field query: Invalid type. Expected: string, given: null"},
},
"bad_content_duplicate_tags": {
"kibana/tags.yml",
[]string{"duplicate tag name 'Tag One' found (SVR00007)"},
},
"bad_kibana_tag_duplicate": {
"kibana/tag/bad_tag-security-solution-default.json",
[]string{"tag name 'Security Solution' is already defined in tags.yml (SVR00007)"},
},
}

for pkgName, test := range tests {
Expand Down
8 changes: 8 additions & 0 deletions docs/validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
| [SVR00004] | Visualization by value |
| [SVR00005] | Minimum Kibana version |
| [SVR00006] | Processor tag is required |
| [SVR00007] | Kibana tag is duplicate |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be some conflicts with this other PR that is also adding the same validation code errors: #1038

https://github.com/elastic/package-spec/pull/1038/files#diff-d8846111760decbd765dc97b91c095ed8264c3f87d42449304e4735930437d1fR25-R26


## JSE00001 - Rename message to event.original
[JSE00001]: #jse00001---rename-message-to-eventoriginal
Expand Down Expand Up @@ -70,3 +71,10 @@ set:
field: event.category
value: [network]
```

## SVR00007 - Kibana tag is duplicate
[SVR00007]: #svr00007---kibana-tag-is-duplicate

**Available since [3.5.5](https://github.com/elastic/package-spec/releases/tag/v3.5.5)**

Kibana tags declared under `kibana/tags.yml` are duplicated or package tags under `kibana/tag` directory are sharing the same id.
5 changes: 5 additions & 0 deletions spec/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
- description: Add support for Kibana `slo_template` assets
type: enhancement
link: https://github.com/elastic/package-spec/pull/1037
- version: 3.5.6-next
changes:
- description: Add validation for Kibana tag duplicates.
type: enhancement
link: https://github.com/elastic/package-spec/pull/1042
- version: 3.5.5
changes:
- description: Run validation semantic rules also in transform fields.
Expand Down
Loading