-
Notifications
You must be signed in to change notification settings - Fork 129
Validate fields based on their mappings and the dynamic templates set #2285
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
Merged
Merged
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
2c3f76e
Add dynamic templates as parameter
mrodm 41d7b88
Add validation for each dynamic template depending on the parameters
mrodm ff5298e
Fixes for dynamic templates
mrodm af04d66
Compare fields with dynamic templates
mrodm f21c712
Remove multi_fields from flattened fields
mrodm aa28db6
Test without filtering dynamic templates
mrodm 3767111
Ensure properties subfields are validated accordingly
mrodm 9fb80b4
Restore filtering and add tests
mrodm 2ad28ac
Disable unmatch_mapping_type and match_mapping_type and continue look…
mrodm 8bdad65
Merge upstream/main into validate-dynamic-mappings
mrodm 151e59e
Refactors and remove log statements
mrodm d0ff6b9
Fix function naming
mrodm b474b4b
Add test with match_pattern regex
mrodm 066b5a0
Add comment in tests
mrodm a7eb191
Remove loading schema from create validator for mappings method
mrodm 7666ac2
Separate parsing and validation of dynamic templates
mrodm a718796
Support match_pattern for the other settings
mrodm 0a5e775
fix test
mrodm 131cc9c
Update tests for multi-fields
mrodm 38db8df
Review multi-fields logic
mrodm 9ff3d0c
Revisit multi-fields logic in ECS
mrodm 5165612
Report all errors related to multi-fields comparing with ECS
mrodm ce557c9
Ignored validation of multi-fields with ECS
mrodm ebda859
Fix multi-field test
mrodm 019ffd4
Rephrase errors
mrodm 8f36c18
Validate fully dynamic objects (preview) with dynamic templates
mrodm d96ffbf
Rephrase debug message
mrodm dbca4fe
Add logging - to be removed
mrodm 8fce0ec
Merge remote-tracking branch 'upstream/main' into validate-dynamic-ma…
mrodm c333707
Revisited log messages
mrodm 0569cfb
Remove comments
mrodm 1aea303
Remove more debug statements
mrodm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| // 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 fields | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "regexp" | ||
| "slices" | ||
| "strings" | ||
|
|
||
| "github.com/elastic/elastic-package/internal/logger" | ||
| ) | ||
|
|
||
| type dynamicTemplate struct { | ||
| name string | ||
| matchPattern string | ||
| match []string | ||
| unmatch []string | ||
| pathMatch []string | ||
| unpathMatch []string | ||
| mapping any | ||
| } | ||
|
|
||
| func (d *dynamicTemplate) Matches(currentPath string, definition map[string]any) (bool, error) { | ||
| fullRegex := d.matchPattern == "regex" | ||
|
|
||
| if len(d.match) > 0 { | ||
| name := fieldNameFromPath(currentPath) | ||
| if !slices.Contains(d.match, name) { | ||
| // If there is no an exact match, it is compared with patterns/wildcards | ||
|
|
||
| // logger.Warnf(">>>> no contained %s: %s", d.match, name) | ||
| matches, err := stringMatchesPatterns(d.match, name, fullRegex) | ||
| if err != nil { | ||
| return false, fmt.Errorf("failed to parse dynamic template %s: %w", d.name, err) | ||
| } | ||
|
|
||
| if !matches { | ||
| // logger.Debugf(">> Issue match: not matches") | ||
| return false, nil | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if len(d.unmatch) > 0 { | ||
| name := fieldNameFromPath(currentPath) | ||
| if slices.Contains(d.unmatch, name) { | ||
| return false, nil | ||
| } | ||
|
|
||
| matches, err := stringMatchesPatterns(d.unmatch, name, fullRegex) | ||
| if err != nil { | ||
| return false, fmt.Errorf("failed to parse dynamic template %s: %w", d.name, err) | ||
| } | ||
|
|
||
| if matches { | ||
| // logger.Debugf(">> Issue unmatch: matches") | ||
| return false, nil | ||
| } | ||
| } | ||
|
|
||
| if len(d.pathMatch) > 0 { | ||
| // logger.Debugf("path_match -> Comparing %s to %q", strings.Join(d.pathMatch, ";"), currentPath) | ||
| matches, err := stringMatchesPatterns(d.pathMatch, currentPath, fullRegex) | ||
| if err != nil { | ||
| return false, fmt.Errorf("failed to parse dynamic template %s: %w", d.name, err) | ||
| } | ||
| if !matches { | ||
| logger.Debugf(">> Issue path_match: not matches (currentPath %s)", currentPath) | ||
| return false, nil | ||
| } | ||
| } | ||
|
|
||
| if len(d.unpathMatch) > 0 { | ||
| matches, err := stringMatchesPatterns(d.unpathMatch, currentPath, fullRegex) | ||
| if err != nil { | ||
| return false, fmt.Errorf("failed to parse dynamic template %s: %w", d.name, err) | ||
| } | ||
| if matches { | ||
| // logger.Debugf(">> Issue unpath_match: matches") | ||
| return false, nil | ||
| } | ||
| } | ||
| return true, nil | ||
| } | ||
|
|
||
| func stringMatchesRegex(regexes []string, elem string) (bool, error) { | ||
| applies := false | ||
| for _, v := range regexes { | ||
| if !strings.Contains(v, "*") { | ||
| // not a regex | ||
| continue | ||
| } | ||
|
|
||
| match, err := regexp.MatchString(v, elem) | ||
| if err != nil { | ||
| return false, fmt.Errorf("failed to build regex %s: %w", v, err) | ||
| } | ||
| if match { | ||
| applies = true | ||
| break | ||
| } | ||
| } | ||
| return applies, nil | ||
| } | ||
|
|
||
| func stringMatchesPatterns(regexes []string, elem string, fullRegex bool) (bool, error) { | ||
| if fullRegex { | ||
| return stringMatchesRegex(regexes, elem) | ||
| } | ||
|
|
||
| // transform wildcards to valid regexes | ||
| updatedRegexes := []string{} | ||
| for _, v := range regexes { | ||
| r := strings.ReplaceAll(v, ".", "\\.") | ||
| r = strings.ReplaceAll(r, "*", ".*") | ||
|
|
||
| // Force to match the beginning and ending of the given path | ||
| r = fmt.Sprintf("^%s$", r) | ||
|
|
||
| updatedRegexes = append(updatedRegexes, r) | ||
| } | ||
| return stringMatchesRegex(updatedRegexes, elem) | ||
| } | ||
|
|
||
| func parseDynamicTemplates(rawDynamicTemplates []map[string]any) ([]dynamicTemplate, error) { | ||
| dynamicTemplates := []dynamicTemplate{} | ||
|
|
||
| for _, template := range rawDynamicTemplates { | ||
| if len(template) != 1 { | ||
| return nil, fmt.Errorf("unexpected number of dynamic template definitions found") | ||
| } | ||
|
|
||
| // there is just one dynamic template per object | ||
| templateName := "" | ||
| var rawContents any | ||
| for key, value := range template { | ||
| templateName = key | ||
| rawContents = value | ||
| } | ||
|
|
||
| if shouldSkipDynamicTemplate(templateName) { | ||
| continue | ||
| } | ||
|
|
||
| aDynamicTemplate := dynamicTemplate{ | ||
| name: templateName, | ||
| } | ||
|
|
||
| // logger.Debugf("Checking dynamic template for %q: %q", currentPath, templateName) | ||
| contents, ok := rawContents.(map[string]any) | ||
| if !ok { | ||
| return nil, fmt.Errorf("unexpected dynamic template format found for %q", templateName) | ||
| } | ||
|
|
||
| for setting, value := range contents { | ||
| switch setting { | ||
| case "mapping": | ||
| aDynamicTemplate.mapping = value | ||
| case "match_pattern": | ||
| s, ok := value.(string) | ||
| if !ok { | ||
| return nil, fmt.Errorf("invalid type for \"match_pattern\": %T", value) | ||
| } | ||
| aDynamicTemplate.matchPattern = s | ||
| case "match": | ||
| // logger.Debugf("> Check match: %q (key %q)", currentPath, name) | ||
| values, err := parseDynamicTemplateParameter(value) | ||
| if err != nil { | ||
| logger.Warnf("failed to check match setting: %s", err) | ||
| return nil, fmt.Errorf("failed to check match setting: %w", err) | ||
| } | ||
| aDynamicTemplate.match = values | ||
| case "unmatch": | ||
| // logger.Debugf("> Check unmatch: %q (key %q)", currentPath, name) | ||
| values, err := parseDynamicTemplateParameter(value) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to check unmatch setting: %w", err) | ||
| } | ||
| aDynamicTemplate.unmatch = values | ||
| case "path_match": | ||
| // logger.Debugf("> Check path_match: %q", currentPath) | ||
| values, err := parseDynamicTemplateParameter(value) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to check path_match setting: %w", err) | ||
| } | ||
| aDynamicTemplate.pathMatch = values | ||
| case "path_unmatch": | ||
| // logger.Debugf("> Check path_unmatch: %q", currentPath) | ||
| values, err := parseDynamicTemplateParameter(value) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to check path_unmatch setting: %w", err) | ||
| } | ||
| aDynamicTemplate.unpathMatch = values | ||
| case "match_mapping_type", "unmatch_mapping_type": | ||
| // Do nothing | ||
| // These parameters require to check the original type (before the document is ingested) | ||
| // but the dynamic template just contains the type from the `mapping` field | ||
| default: | ||
| return nil, fmt.Errorf("unexpected setting found in dynamic template") | ||
| } | ||
| } | ||
|
|
||
| dynamicTemplates = append(dynamicTemplates, aDynamicTemplate) | ||
| } | ||
|
|
||
| return dynamicTemplates, nil | ||
| } | ||
|
|
||
| func shouldSkipDynamicTemplate(templateName string) bool { | ||
| // Filter out dynamic templates created by elastic-package (import_mappings) | ||
| // or added automatically by ecs@mappings component template | ||
| if strings.HasPrefix(templateName, "_embedded_ecs-") { | ||
| return true | ||
| } | ||
| if strings.HasPrefix(templateName, "ecs_") { | ||
| return true | ||
| } | ||
| if slices.Contains([]string{"all_strings_to_keywords", "strings_as_keyword"}, templateName) { | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func parseDynamicTemplateParameter(value any) ([]string, error) { | ||
| all := []string{} | ||
| switch v := value.(type) { | ||
| case []any: | ||
| for _, elem := range v { | ||
| s, ok := elem.(string) | ||
| if !ok { | ||
| return nil, fmt.Errorf("failed to cast to string: %s", elem) | ||
| } | ||
| all = append(all, s) | ||
| } | ||
| case any: | ||
| s, ok := v.(string) | ||
| if !ok { | ||
| return nil, fmt.Errorf("failed to cast to string: %s", v) | ||
| } | ||
| all = append(all, s) | ||
| default: | ||
| return nil, fmt.Errorf("unexpected type for setting: %T", value) | ||
|
|
||
| } | ||
| return all, nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit. Could we leverage https://pkg.go.dev/github.com/mitchellh/mapstructure to convert from
map[string]anyto the struct?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how to take advantage of that library here.
Some fields need to take into account if they come from an string o a list of strings.
And looking at this example, https://pkg.go.dev/github.com/go-viper/mapstructure/v2#example-Decode-DecodeHookFunc
It looks like, I would need to do the same logic inside that function. For now, I think I would keep as it is if it is not an issue.