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

Validate fields based on their mappings and the dynamic templates set #2285

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2c3f76e
Add dynamic templates as parameter
mrodm Dec 12, 2024
41d7b88
Add validation for each dynamic template depending on the parameters
mrodm Dec 16, 2024
ff5298e
Fixes for dynamic templates
mrodm Dec 16, 2024
af04d66
Compare fields with dynamic templates
mrodm Dec 17, 2024
f21c712
Remove multi_fields from flattened fields
mrodm Dec 17, 2024
aa28db6
Test without filtering dynamic templates
mrodm Dec 17, 2024
3767111
Ensure properties subfields are validated accordingly
mrodm Dec 18, 2024
9fb80b4
Restore filtering and add tests
mrodm Dec 18, 2024
2ad28ac
Disable unmatch_mapping_type and match_mapping_type and continue look…
mrodm Dec 19, 2024
8bdad65
Merge upstream/main into validate-dynamic-mappings
mrodm Dec 19, 2024
151e59e
Refactors and remove log statements
mrodm Dec 19, 2024
d0ff6b9
Fix function naming
mrodm Dec 19, 2024
b474b4b
Add test with match_pattern regex
mrodm Dec 19, 2024
066b5a0
Add comment in tests
mrodm Dec 19, 2024
a7eb191
Remove loading schema from create validator for mappings method
mrodm Jan 7, 2025
7666ac2
Separate parsing and validation of dynamic templates
mrodm Jan 7, 2025
a718796
Support match_pattern for the other settings
mrodm Jan 7, 2025
0a5e775
fix test
mrodm Jan 8, 2025
131cc9c
Update tests for multi-fields
mrodm Jan 8, 2025
38db8df
Review multi-fields logic
mrodm Jan 8, 2025
9ff3d0c
Revisit multi-fields logic in ECS
mrodm Jan 8, 2025
5165612
Report all errors related to multi-fields comparing with ECS
mrodm Jan 8, 2025
ce557c9
Ignored validation of multi-fields with ECS
mrodm Jan 8, 2025
ebda859
Fix multi-field test
mrodm Jan 8, 2025
019ffd4
Rephrase errors
mrodm Jan 9, 2025
8f36c18
Validate fully dynamic objects (preview) with dynamic templates
mrodm Jan 9, 2025
d96ffbf
Rephrase debug message
mrodm Jan 9, 2025
dbca4fe
Add logging - to be removed
mrodm Jan 9, 2025
8fce0ec
Merge remote-tracking branch 'upstream/main' into validate-dynamic-ma…
mrodm Jan 14, 2025
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
249 changes: 249 additions & 0 deletions internal/fields/dynamic_template.go
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
}
Loading