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

Check allowed values for fields #771

Merged
merged 10 commits into from
Jun 9, 2022
58 changes: 46 additions & 12 deletions internal/fields/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,25 @@ import (
"strings"

"gopkg.in/yaml.v3"

"github.com/elastic/elastic-package/internal/common"
)

// FieldDefinition describes a single field with its properties.
type FieldDefinition struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Type string `yaml:"type"`
Value string `yaml:"value"` // The value to associate with a constant_keyword field.
Pattern string `yaml:"pattern"`
Unit string `yaml:"unit"`
MetricType string `yaml:"metric_type"`
External string `yaml:"external"`
Index *bool `yaml:"index"`
DocValues *bool `yaml:"doc_values"`
Fields FieldDefinitions `yaml:"fields,omitempty"`
MultiFields []FieldDefinition `yaml:"multi_fields,omitempty"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Type string `yaml:"type"`
Value string `yaml:"value"` // The value to associate with a constant_keyword field.
AllowedValues AllowedValues `yaml:"allowed_values"`
Pattern string `yaml:"pattern"`
Unit string `yaml:"unit"`
MetricType string `yaml:"metric_type"`
External string `yaml:"external"`
Index *bool `yaml:"index"`
DocValues *bool `yaml:"doc_values"`
Fields FieldDefinitions `yaml:"fields,omitempty"`
MultiFields []FieldDefinition `yaml:"multi_fields,omitempty"`
}

func (orig *FieldDefinition) Update(fd FieldDefinition) {
Expand All @@ -40,6 +43,9 @@ func (orig *FieldDefinition) Update(fd FieldDefinition) {
if fd.Value != "" {
orig.Value = fd.Value
}
if len(fd.AllowedValues) > 0 {
orig.AllowedValues = fd.AllowedValues
}
if fd.Pattern != "" {
orig.Pattern = fd.Pattern
}
Expand Down Expand Up @@ -182,3 +188,31 @@ func cleanNested(parent *FieldDefinition) (base []FieldDefinition) {
parent.Fields = nested
return base
}

// AllowedValues is the list of allowed values for a field.
type AllowedValues []AllowedValue

// Allowed returns true if a given value is allowed.
func (avs AllowedValues) IsAllowed(value string) bool {
if len(avs) == 0 {
// No configured allowed values, any value is allowed.
return true
}
return common.StringSliceContains(avs.Values(), value)
}

// Values returns the list of allowed values.
func (avs AllowedValues) Values() []string {
var values []string
for _, v := range avs {
values = append(values, v.Name)
}
return values
}

// AllowedValue is one of the allowed values for a field.
type AllowedValue struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
ExpectedEventTypes []string `yaml:"expected_event_types"`
}
15 changes: 15 additions & 0 deletions internal/fields/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@ func (v *Validator) parseSingleElementValue(key string, definition FieldDefiniti
if err := ensurePatternMatches(key, valStr, definition.Pattern); err != nil {
return err
}
if err := ensureAllowedValues(key, valStr, definition.AllowedValues); err != nil {
return err
}
// Normal text fields should be of type string.
// If a pattern is provided, it checks if the value matches.
case "keyword", "text":
Expand All @@ -402,6 +405,9 @@ func (v *Validator) parseSingleElementValue(key string, definition FieldDefiniti
if err := ensurePatternMatches(key, valStr, definition.Pattern); err != nil {
return err
}
if err := ensureAllowedValues(key, valStr, definition.AllowedValues); err != nil {
return err
}
// Dates are expected to be formatted as strings or as seconds or milliseconds
// since epoch.
// If it is a string and a pattern is provided, it checks if the value matches.
Expand Down Expand Up @@ -531,3 +537,12 @@ func ensureConstantKeywordValueMatches(key, value, constantKeywordValue string)
}
return nil
}

// ensureAllowedValues validates that the document's field value
// is one of the allowed values.
func ensureAllowedValues(key, value string, allowedValues AllowedValues) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: in theory, you don't need to pass key here as it's just used to render the error message :)

Copy link
Member Author

@jsoriano jsoriano Apr 6, 2022

Choose a reason for hiding this comment

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

Umm, but error message is rendered here, key is used in this function for that.

I would be fine with refactoring this error generation, but in a different PR, making the other ensure methods consistent with this.

if !allowedValues.IsAllowed(value) {
return fmt.Errorf("field %q's value %q is not one of the allowed values (%s)", key, value, strings.Join(allowedValues.Values(), ", "))
}
return nil
}
48 changes: 48 additions & 0 deletions internal/fields/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,54 @@ func Test_parseElementValue(t *testing.T) {
},
fail: true,
},
// allowed values
{
key: "allowed values",
value: "configuration",
definition: FieldDefinition{
Type: "keyword",
AllowedValues: AllowedValues{
{
Name: "configuration",
},
{
Name: "network",
},
},
},
},
{
key: "not allowed value",
value: "display",
definition: FieldDefinition{
Type: "keyword",
AllowedValues: AllowedValues{
{
Name: "configuration",
},
{
Name: "network",
},
},
},
fail: true,
},
{
key: "not allowed value in array",
value: []string{"configuration", "display"},
definition: FieldDefinition{
Type: "keyword",
AllowedValues: AllowedValues{
{
Name: "configuration",
},
{
Name: "network",
},
},
},
fail: true,
},
// fields shouldn't be stored in groups
{
key: "host",
Expand Down