internal/fields/dynamictemplate.go (200 lines of code) (raw):
// 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
matches, err := stringMatchesPatterns(d.match, name, fullRegex)
if err != nil {
return false, fmt.Errorf("failed to parse dynamic template %q: %w", d.name, err)
}
if !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 %q: %w", d.name, err)
}
if matches {
return false, nil
}
}
if len(d.pathMatch) > 0 {
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 {
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 %q: %w", d.name, err)
}
if 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,
}
contents, ok := rawContents.(map[string]any)
if !ok {
return nil, fmt.Errorf("unexpected dynamic template format found for %q", templateName)
}
isRuntime := false
for setting, value := range contents {
switch setting {
case "mapping":
aDynamicTemplate.mapping = value
case "runtime":
isRuntime = true
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":
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":
values, err := parseDynamicTemplateParameter(value)
if err != nil {
return nil, fmt.Errorf("failed to check unmatch setting: %w", err)
}
aDynamicTemplate.unmatch = values
case "path_match":
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":
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")
}
}
if isRuntime {
continue
}
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
}