internal/builder/dynamic_mappings.go (206 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 builder
import (
_ "embed"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3"
"github.com/elastic/elastic-package/internal/formatter"
"github.com/elastic/elastic-package/internal/logger"
"github.com/elastic/elastic-package/internal/packages"
"github.com/elastic/elastic-package/internal/packages/buildmanifest"
)
//go:embed _static/ecs_mappings.yaml
var staticEcsMappings string
const prefixMapping = "_embedded_ecs"
var semver2_3_0 = semver.MustParse("2.3.0")
type ecsTemplates struct {
Mappings struct {
DynamicTemplates []map[string]interface{} `yaml:"dynamic_templates"`
} `yaml:"mappings"`
}
func addDynamicMappings(packageRoot, destinationDir string) error {
packageManifest := filepath.Join(destinationDir, packages.PackageManifestFile)
m, err := packages.ReadPackageManifest(packageManifest)
if err != nil {
return err
}
shouldImport, err := shouldImportEcsMappings(m.SpecVersion, packageRoot)
if err != nil {
return err
}
if !shouldImport {
return nil
}
logger.Info("Import ECS mappings into the built package (technical preview)")
switch m.Type {
case "integration":
dataStreamManifests, err := filepath.Glob(filepath.Join(destinationDir, "data_stream", "*", packages.DataStreamManifestFile))
if err != nil {
return err
}
for _, datastream := range dataStreamManifests {
contents, err := addDynamicMappingElements(datastream)
if err != nil {
return err
}
err = os.WriteFile(datastream, contents, 0664)
if err != nil {
return err
}
}
case "input":
contents, err := addDynamicMappingElements(packageManifest)
if err != nil {
return err
}
err = os.WriteFile(packageManifest, contents, 0664)
if err != nil {
return err
}
}
return nil
}
func shouldImportEcsMappings(specVersion, packageRoot string) (bool, error) {
v, err := semver.NewVersion(specVersion)
if err != nil {
return false, fmt.Errorf("invalid spec version: %w", err)
}
bm, ok, err := buildmanifest.ReadBuildManifest(packageRoot)
if err != nil {
return false, fmt.Errorf("can't read build manifest: %w", err)
}
if !ok {
logger.Debug("Build manifest hasn't been defined for the package")
return false, nil
}
if !bm.ImportMappings() {
logger.Debug("Package doesn't have to import ECS mappings")
return false, nil
}
if v.LessThan(semver2_3_0) {
logger.Debugf("Required spec version >= %s to import ECS mappings", semver2_3_0.String())
return false, nil
}
return true, nil
}
func addDynamicMappingElements(path string) ([]byte, error) {
ecsMappings, err := loadEcsMappings()
if err != nil {
return nil, errors.New("can't load ecs mappings template")
}
contents, err := os.ReadFile(path)
if err != nil {
return nil, errors.New("can't read manifest")
}
var doc yaml.Node
err = yaml.Unmarshal(contents, &doc)
if err != nil {
return nil, err
}
err = addEcsMappings(&doc, ecsMappings)
if err != nil {
return nil, err
}
contents, err = formatResult(&doc)
if err != nil {
return nil, err
}
return contents, nil
}
func loadEcsMappings() (ecsTemplates, error) {
var ecsMappings ecsTemplates
err := yaml.Unmarshal([]byte(staticEcsMappings), &ecsMappings)
if err != nil {
return ecsMappings, err
}
return ecsMappings, nil
}
func addEcsMappings(doc *yaml.Node, mappings ecsTemplates) error {
var templates yaml.Node
err := templates.Encode(mappings.Mappings.DynamicTemplates)
if err != nil {
return fmt.Errorf("failed to encode dynamic templates: %w", err)
}
renameMappingsNames(&templates)
err = appendElements(doc, []string{"elasticsearch", "index_template", "mappings", "dynamic_templates"}, &templates)
if err != nil {
return fmt.Errorf("failed to append dynamic templates: %w", err)
}
return nil
}
func renameMappingsNames(doc *yaml.Node) {
switch doc.Kind {
case yaml.MappingNode:
for i := 0; i < len(doc.Content); i += 2 {
doc.Content[i].Value = fmt.Sprintf("%s-%s", prefixMapping, doc.Content[i].Value)
}
case yaml.SequenceNode:
for i := 0; i < len(doc.Content); i++ {
renameMappingsNames(doc.Content[i])
}
case yaml.DocumentNode:
renameMappingsNames(doc.Content[0])
}
}
func appendElements(root *yaml.Node, path []string, values *yaml.Node) error {
if len(path) == 0 {
contents := values.Content
if values.Kind == yaml.DocumentNode {
contents = values.Content[0].Content
}
root.Content = append(root.Content, contents...)
return nil
}
key := path[0]
rest := path[1:]
switch root.Kind {
case yaml.DocumentNode:
return appendElements(root.Content[0], path, values)
case yaml.MappingNode:
for i := 0; i < len(root.Content); i += 2 {
child := root.Content[i]
if child.Value == key {
return appendElements(root.Content[i+1], rest, values)
}
}
newContentNodes := newYamlNode(key)
root.Content = append(root.Content, newContentNodes...)
return appendElements(newContentNodes[1], rest, values)
case yaml.SequenceNode:
index, err := strconv.Atoi(key)
if err != nil {
return err
}
if len(root.Content) >= index {
return fmt.Errorf("index out of range in nodes from key %s", key)
}
return appendElements(root.Content[index], rest, values)
}
return nil
}
func newYamlNode(key string) []*yaml.Node {
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: key}
var childNode *yaml.Node
switch key {
case "dynamic_templates":
childNode = &yaml.Node{Kind: yaml.SequenceNode, Value: key}
default:
childNode = &yaml.Node{Kind: yaml.MappingNode, Value: key}
}
return []*yaml.Node{keyNode, childNode}
}
func formatResult(result interface{}) ([]byte, error) {
d, err := yaml.Marshal(result)
if err != nil {
return nil, errors.New("failed to encode")
}
yamlFormatter := formatter.NewYAMLFormatter(formatter.KeysWithDotActionNone)
d, _, err = yamlFormatter.Format(d)
if err != nil {
return nil, errors.New("failed to format")
}
return d, nil
}