diff --git a/internal/fields/model.go b/internal/fields/model.go index 4613bff89..5665605f7 100644 --- a/internal/fields/model.go +++ b/internal/fields/model.go @@ -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) { @@ -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 } @@ -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"` +} diff --git a/internal/fields/validate.go b/internal/fields/validate.go index 5d9b6d0fd..89a1f803b 100644 --- a/internal/fields/validate.go +++ b/internal/fields/validate.go @@ -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": @@ -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. @@ -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 { + 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 +} diff --git a/internal/fields/validate_test.go b/internal/fields/validate_test.go index 71d9d4a3f..8854bf43c 100644 --- a/internal/fields/validate_test.go +++ b/internal/fields/validate_test.go @@ -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",