Skip to content

Commit c975508

Browse files
authored
Validate fields based on their mappings and the dynamic templates set (#2285)
Added validation of the fields based on the mappings and dynamic templates found. This comparison is based on the values found in the preview of the index template with the values found in the index/datastream after being indexed the documents of the given test.
1 parent b7acb9f commit c975508

File tree

5 files changed

+576
-219
lines changed

5 files changed

+576
-219
lines changed

internal/fields/dynamic_template.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package fields
6+
7+
import (
8+
"fmt"
9+
"regexp"
10+
"slices"
11+
"strings"
12+
13+
"github.com/elastic/elastic-package/internal/logger"
14+
)
15+
16+
type dynamicTemplate struct {
17+
name string
18+
matchPattern string
19+
match []string
20+
unmatch []string
21+
pathMatch []string
22+
unpathMatch []string
23+
mapping any
24+
}
25+
26+
func (d *dynamicTemplate) Matches(currentPath string, definition map[string]any) (bool, error) {
27+
fullRegex := d.matchPattern == "regex"
28+
29+
if len(d.match) > 0 {
30+
name := fieldNameFromPath(currentPath)
31+
if !slices.Contains(d.match, name) {
32+
// If there is no an exact match, it is compared with patterns/wildcards
33+
matches, err := stringMatchesPatterns(d.match, name, fullRegex)
34+
if err != nil {
35+
return false, fmt.Errorf("failed to parse dynamic template %q: %w", d.name, err)
36+
}
37+
38+
if !matches {
39+
return false, nil
40+
}
41+
}
42+
}
43+
44+
if len(d.unmatch) > 0 {
45+
name := fieldNameFromPath(currentPath)
46+
if slices.Contains(d.unmatch, name) {
47+
return false, nil
48+
}
49+
50+
matches, err := stringMatchesPatterns(d.unmatch, name, fullRegex)
51+
if err != nil {
52+
return false, fmt.Errorf("failed to parse dynamic template %q: %w", d.name, err)
53+
}
54+
55+
if matches {
56+
return false, nil
57+
}
58+
}
59+
60+
if len(d.pathMatch) > 0 {
61+
// logger.Debugf("path_match -> Comparing %s to %q", strings.Join(d.pathMatch, ";"), currentPath)
62+
matches, err := stringMatchesPatterns(d.pathMatch, currentPath, fullRegex)
63+
if err != nil {
64+
return false, fmt.Errorf("failed to parse dynamic template %s: %w", d.name, err)
65+
}
66+
if !matches {
67+
return false, nil
68+
}
69+
}
70+
71+
if len(d.unpathMatch) > 0 {
72+
matches, err := stringMatchesPatterns(d.unpathMatch, currentPath, fullRegex)
73+
if err != nil {
74+
return false, fmt.Errorf("failed to parse dynamic template %q: %w", d.name, err)
75+
}
76+
if matches {
77+
return false, nil
78+
}
79+
}
80+
return true, nil
81+
}
82+
83+
func stringMatchesRegex(regexes []string, elem string) (bool, error) {
84+
applies := false
85+
for _, v := range regexes {
86+
if !strings.Contains(v, "*") {
87+
// not a regex
88+
continue
89+
}
90+
91+
match, err := regexp.MatchString(v, elem)
92+
if err != nil {
93+
return false, fmt.Errorf("failed to build regex %s: %w", v, err)
94+
}
95+
if match {
96+
applies = true
97+
break
98+
}
99+
}
100+
return applies, nil
101+
}
102+
103+
func stringMatchesPatterns(regexes []string, elem string, fullRegex bool) (bool, error) {
104+
if fullRegex {
105+
return stringMatchesRegex(regexes, elem)
106+
}
107+
108+
// transform wildcards to valid regexes
109+
updatedRegexes := []string{}
110+
for _, v := range regexes {
111+
r := strings.ReplaceAll(v, ".", "\\.")
112+
r = strings.ReplaceAll(r, "*", ".*")
113+
114+
// Force to match the beginning and ending of the given path
115+
r = fmt.Sprintf("^%s$", r)
116+
117+
updatedRegexes = append(updatedRegexes, r)
118+
}
119+
return stringMatchesRegex(updatedRegexes, elem)
120+
}
121+
122+
func parseDynamicTemplates(rawDynamicTemplates []map[string]any) ([]dynamicTemplate, error) {
123+
dynamicTemplates := []dynamicTemplate{}
124+
125+
for _, template := range rawDynamicTemplates {
126+
if len(template) != 1 {
127+
return nil, fmt.Errorf("unexpected number of dynamic template definitions found")
128+
}
129+
130+
// there is just one dynamic template per object
131+
templateName := ""
132+
var rawContents any
133+
for key, value := range template {
134+
templateName = key
135+
rawContents = value
136+
}
137+
138+
if shouldSkipDynamicTemplate(templateName) {
139+
continue
140+
}
141+
142+
aDynamicTemplate := dynamicTemplate{
143+
name: templateName,
144+
}
145+
146+
contents, ok := rawContents.(map[string]any)
147+
if !ok {
148+
return nil, fmt.Errorf("unexpected dynamic template format found for %q", templateName)
149+
}
150+
151+
for setting, value := range contents {
152+
switch setting {
153+
case "mapping":
154+
aDynamicTemplate.mapping = value
155+
case "match_pattern":
156+
s, ok := value.(string)
157+
if !ok {
158+
return nil, fmt.Errorf("invalid type for \"match_pattern\": %T", value)
159+
}
160+
aDynamicTemplate.matchPattern = s
161+
case "match":
162+
values, err := parseDynamicTemplateParameter(value)
163+
if err != nil {
164+
logger.Warnf("failed to check match setting: %s", err)
165+
return nil, fmt.Errorf("failed to check match setting: %w", err)
166+
}
167+
aDynamicTemplate.match = values
168+
case "unmatch":
169+
values, err := parseDynamicTemplateParameter(value)
170+
if err != nil {
171+
return nil, fmt.Errorf("failed to check unmatch setting: %w", err)
172+
}
173+
aDynamicTemplate.unmatch = values
174+
case "path_match":
175+
values, err := parseDynamicTemplateParameter(value)
176+
if err != nil {
177+
return nil, fmt.Errorf("failed to check path_match setting: %w", err)
178+
}
179+
aDynamicTemplate.pathMatch = values
180+
case "path_unmatch":
181+
values, err := parseDynamicTemplateParameter(value)
182+
if err != nil {
183+
return nil, fmt.Errorf("failed to check path_unmatch setting: %w", err)
184+
}
185+
aDynamicTemplate.unpathMatch = values
186+
case "match_mapping_type", "unmatch_mapping_type":
187+
// Do nothing
188+
// These parameters require to check the original type (before the document is ingested)
189+
// but the dynamic template just contains the type from the `mapping` field
190+
default:
191+
return nil, fmt.Errorf("unexpected setting found in dynamic template")
192+
}
193+
}
194+
195+
dynamicTemplates = append(dynamicTemplates, aDynamicTemplate)
196+
}
197+
198+
return dynamicTemplates, nil
199+
}
200+
201+
func shouldSkipDynamicTemplate(templateName string) bool {
202+
// Filter out dynamic templates created by elastic-package (import_mappings)
203+
// or added automatically by ecs@mappings component template
204+
if strings.HasPrefix(templateName, "_embedded_ecs-") {
205+
return true
206+
}
207+
if strings.HasPrefix(templateName, "ecs_") {
208+
return true
209+
}
210+
if slices.Contains([]string{"all_strings_to_keywords", "strings_as_keyword"}, templateName) {
211+
return true
212+
}
213+
return false
214+
}
215+
216+
func parseDynamicTemplateParameter(value any) ([]string, error) {
217+
all := []string{}
218+
switch v := value.(type) {
219+
case []any:
220+
for _, elem := range v {
221+
s, ok := elem.(string)
222+
if !ok {
223+
return nil, fmt.Errorf("failed to cast to string: %s", elem)
224+
}
225+
all = append(all, s)
226+
}
227+
case any:
228+
s, ok := v.(string)
229+
if !ok {
230+
return nil, fmt.Errorf("failed to cast to string: %s", v)
231+
}
232+
all = append(all, s)
233+
default:
234+
return nil, fmt.Errorf("unexpected type for setting: %T", value)
235+
236+
}
237+
return all, nil
238+
}

0 commit comments

Comments
 (0)