Skip to content

Commit 6bb35c1

Browse files
authored
Configurable index template loading (#21212)
## What does this PR do? The PR adds a new configuration option named `setup.template.type` to select the index template type. From ES v7.8 new index templates were introduced. Possible option: * `legacy`: Loads the legacy index template. This is the default option, so it does not break existing deployments. * `component`: This loads Beats' index template as a composite template, so it can be used in the users' index templates. * `index`: Loads the new index template. ## Why is it important? Index templates v2 was released in Elasticsearch 7.8. Previously Beats had used the legacy endpoint for installing index templates. Now we are moving to the newer version. Closes #17829
1 parent 1a80170 commit 6bb35c1

20 files changed

+282
-53
lines changed

CHANGELOG.next.asciidoc

+1
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d
437437
- Added experimental dataset `juniper/netscreen`. {pull}20820[20820]
438438
- Added experimental dataset `sophos/utm`. {pull}20820[20820]
439439
- Add Cloud Foundry tags in related events. {pull}21177[21177]
440+
- Add option to select the type of index template to load: legacy, component, index. {pull}21212[21212]
440441

441442
*Auditbeat*
442443

auditbeat/auditbeat.reference.yml

+5
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,11 @@ output.elasticsearch:
11441144
# Set to false to disable template loading.
11451145
#setup.template.enabled: true
11461146

1147+
# Select the kind of index template. From Elasticsearch 7.8, it is possible to
1148+
# use component templates. Available options: legacy, component, index.
1149+
# By default auditbeat uses the legacy index templates.
1150+
#setup.template.type: legacy
1151+
11471152
# Template name. By default the template name is "auditbeat-%{[agent.version]}"
11481153
# The template name and pattern has to be set in case the Elasticsearch index pattern is modified.
11491154
#setup.template.name: "auditbeat-%{[agent.version]}"

filebeat/filebeat.reference.yml

+5
Original file line numberDiff line numberDiff line change
@@ -1870,6 +1870,11 @@ output.elasticsearch:
18701870
# Set to false to disable template loading.
18711871
#setup.template.enabled: true
18721872

1873+
# Select the kind of index template. From Elasticsearch 7.8, it is possible to
1874+
# use component templates. Available options: legacy, component, index.
1875+
# By default filebeat uses the legacy index templates.
1876+
#setup.template.type: legacy
1877+
18731878
# Template name. By default the template name is "filebeat-%{[agent.version]}"
18741879
# The template name and pattern has to be set in case the Elasticsearch index pattern is modified.
18751880
#setup.template.name: "filebeat-%{[agent.version]}"

heartbeat/heartbeat.reference.yml

+5
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,11 @@ output.elasticsearch:
13211321
# Set to false to disable template loading.
13221322
#setup.template.enabled: true
13231323

1324+
# Select the kind of index template. From Elasticsearch 7.8, it is possible to
1325+
# use component templates. Available options: legacy, component, index.
1326+
# By default heartbeat uses the legacy index templates.
1327+
#setup.template.type: legacy
1328+
13241329
# Template name. By default the template name is "heartbeat-%{[agent.version]}"
13251330
# The template name and pattern has to be set in case the Elasticsearch index pattern is modified.
13261331
#setup.template.name: "heartbeat-%{[agent.version]}"

journalbeat/journalbeat.reference.yml

+5
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,11 @@ output.elasticsearch:
10861086
# Set to false to disable template loading.
10871087
#setup.template.enabled: true
10881088

1089+
# Select the kind of index template. From Elasticsearch 7.8, it is possible to
1090+
# use component templates. Available options: legacy, component, index.
1091+
# By default journalbeat uses the legacy index templates.
1092+
#setup.template.type: legacy
1093+
10891094
# Template name. By default the template name is "journalbeat-%{[agent.version]}"
10901095
# The template name and pattern has to be set in case the Elasticsearch index pattern is modified.
10911096
#setup.template.name: "journalbeat-%{[agent.version]}"

libbeat/_meta/config/setup.template.reference.yml.tmpl

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
# Set to false to disable template loading.
88
#setup.template.enabled: true
99

10+
# Select the kind of index template. From Elasticsearch 7.8, it is possible to
11+
# use component templates. Available options: legacy, component, index.
12+
# By default {{.BeatName}} uses the legacy index templates.
13+
#setup.template.type: legacy
14+
1015
# Template name. By default the template name is "{{.BeatIndexPrefix}}-%{[agent.version]}"
1116
# The template name and pattern has to be set in case the Elasticsearch index pattern is modified.
1217
#setup.template.name: "{{.BeatIndexPrefix}}-%{[agent.version]}"

libbeat/docs/template-config.asciidoc

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ existing one.
2626
*`setup.template.enabled`*:: Set to false to disable template loading. If set this to false,
2727
you must <<load-template-manually,load the template manually>>.
2828

29+
*`setup.template.type`*:: The type of template to use. Available options: `legacy` (default), index templates
30+
before Elasticsearch v7.8. Use this to avoid breaking existing deployments. New options are `composite`
31+
and `index`. Selecting `component` loads a component template which can be included in new index templates.
32+
The option `index` loads the new index template.
33+
2934
*`setup.template.name`*:: The name of the template. The default is
3035
+{beatname_lc}+. The {beatname_uc} version is always appended to the given
3136
name, so the final name is +{beatname_lc}-%{[{beat_version_key}]}+.

libbeat/template/config.go

+48-8
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,27 @@
1717

1818
package template
1919

20-
import "github.com/elastic/beats/v7/libbeat/mapping"
20+
import (
21+
"fmt"
22+
23+
"github.com/elastic/beats/v7/libbeat/mapping"
24+
)
25+
26+
const (
27+
IndexTemplateLegacy IndexTemplateType = iota
28+
IndexTemplateComponent
29+
IndexTemplateIndex
30+
)
31+
32+
var (
33+
templateTypes = map[string]IndexTemplateType{
34+
"legacy": IndexTemplateLegacy,
35+
"component": IndexTemplateComponent,
36+
"index": IndexTemplateIndex,
37+
}
38+
)
39+
40+
type IndexTemplateType uint8
2141

2242
// TemplateConfig holds config information about the Elasticsearch template
2343
type TemplateConfig struct {
@@ -30,10 +50,12 @@ type TemplateConfig struct {
3050
Path string `config:"path"`
3151
Name string `config:"name"`
3252
} `config:"json"`
33-
AppendFields mapping.Fields `config:"append_fields"`
34-
Overwrite bool `config:"overwrite"`
35-
Settings TemplateSettings `config:"settings"`
36-
Order int `config:"order"`
53+
AppendFields mapping.Fields `config:"append_fields"`
54+
Overwrite bool `config:"overwrite"`
55+
Settings TemplateSettings `config:"settings"`
56+
Order int `config:"order"`
57+
Priority int `config:"priority"`
58+
Type IndexTemplateType `config:"type"`
3759
}
3860

3961
// TemplateSettings are part of the Elasticsearch template and hold index and source specific information.
@@ -45,8 +67,26 @@ type TemplateSettings struct {
4567
// DefaultConfig for index template
4668
func DefaultConfig() TemplateConfig {
4769
return TemplateConfig{
48-
Enabled: true,
49-
Fields: "",
50-
Order: 1,
70+
Enabled: true,
71+
Fields: "",
72+
Type: IndexTemplateLegacy,
73+
Order: 1,
74+
Priority: 150,
5175
}
5276
}
77+
78+
func (t *IndexTemplateType) Unpack(v string) error {
79+
if v == "" {
80+
*t = IndexTemplateLegacy
81+
return nil
82+
}
83+
84+
var tt IndexTemplateType
85+
var ok bool
86+
if tt, ok = templateTypes[v]; !ok {
87+
return fmt.Errorf("unknown index template type: %s", v)
88+
}
89+
*t = tt
90+
91+
return nil
92+
}

libbeat/template/load.go

+20-6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ import (
3131
"github.com/elastic/beats/v7/libbeat/paths"
3232
)
3333

34+
var (
35+
templateLoaderPath = map[IndexTemplateType]string{
36+
IndexTemplateLegacy: "/_template/",
37+
IndexTemplateComponent: "/_component_template/",
38+
IndexTemplateIndex: "/_index_template/",
39+
}
40+
)
41+
3442
//Loader interface for loading templates
3543
type Loader interface {
3644
Load(config TemplateConfig, info beat.Info, fields []byte, migration bool) error
@@ -97,7 +105,7 @@ func (l *ESLoader) Load(config TemplateConfig, info beat.Info, fields []byte, mi
97105
templateName = config.JSON.Name
98106
}
99107

100-
if l.templateExists(templateName) && !config.Overwrite {
108+
if l.templateExists(templateName, config.Type) && !config.Overwrite {
101109
l.log.Infof("Template %s already exists and will not be overwritten.", templateName)
102110
return nil
103111
}
@@ -107,7 +115,7 @@ func (l *ESLoader) Load(config TemplateConfig, info beat.Info, fields []byte, mi
107115
if err != nil {
108116
return err
109117
}
110-
if err := l.loadTemplate(templateName, body); err != nil {
118+
if err := l.loadTemplate(templateName, config.Type, body); err != nil {
111119
return fmt.Errorf("could not load template. Elasticsearch returned: %v. Template is: %s", err, body.StringToPrint())
112120
}
113121
l.log.Infof("template with name '%s' loaded.", templateName)
@@ -117,10 +125,11 @@ func (l *ESLoader) Load(config TemplateConfig, info beat.Info, fields []byte, mi
117125
// loadTemplate loads a template into Elasticsearch overwriting the existing
118126
// template if it exists. If you wish to not overwrite an existing template
119127
// then use CheckTemplate prior to calling this method.
120-
func (l *ESLoader) loadTemplate(templateName string, template map[string]interface{}) error {
128+
func (l *ESLoader) loadTemplate(templateName string, templateType IndexTemplateType, template map[string]interface{}) error {
121129
l.log.Infof("Try loading template %s to Elasticsearch", templateName)
122-
path := "/_template/" + templateName
123-
params := esVersionParams(l.client.GetVersion())
130+
clientVersion := l.client.GetVersion()
131+
path := templateLoaderPath[templateType] + templateName
132+
params := esVersionParams(clientVersion)
124133
status, body, err := l.client.Request("PUT", path, "", params, template)
125134
if err != nil {
126135
return fmt.Errorf("couldn't load template: %v. Response body: %s", err, body)
@@ -133,11 +142,16 @@ func (l *ESLoader) loadTemplate(templateName string, template map[string]interfa
133142

134143
// templateExists checks if a given template already exist. It returns true if
135144
// and only if Elasticsearch returns with HTTP status code 200.
136-
func (l *ESLoader) templateExists(templateName string) bool {
145+
func (l *ESLoader) templateExists(templateName string, templateType IndexTemplateType) bool {
137146
if l.client == nil {
138147
return false
139148
}
140149

150+
if templateType == IndexTemplateComponent {
151+
status, _, _ := l.client.Request("GET", "/_component_template/"+templateName, "", nil, nil)
152+
return status == http.StatusOK
153+
}
154+
141155
status, body, _ := l.client.Request("GET", "/_cat/templates/"+templateName, "", nil, nil)
142156

143157
return status == http.StatusOK && strings.Contains(string(body), templateName)

libbeat/template/load_integration_test.go

+40-16
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ func newTestSetup(t *testing.T, cfg TemplateConfig) *testSetup {
6565
t.Fatal(err)
6666
}
6767
s := testSetup{t: t, client: client, loader: NewESLoader(client), config: cfg}
68-
client.Request("DELETE", "/_template/"+cfg.Name, "", nil, nil)
69-
require.False(t, s.loader.templateExists(cfg.Name))
68+
client.Request("DELETE", templateLoaderPath[cfg.Type]+cfg.Name, "", nil, nil)
69+
require.False(t, s.loader.templateExists(cfg.Name, cfg.Type))
7070
return &s
7171
}
7272
func (ts *testSetup) loadFromFile(fileElems []string) error {
@@ -82,7 +82,7 @@ func (ts *testSetup) load(fields []byte) error {
8282

8383
func (ts *testSetup) mustLoad(fields []byte) {
8484
require.NoError(ts.t, ts.load(fields))
85-
require.True(ts.t, ts.loader.templateExists(ts.config.Name))
85+
require.True(ts.t, ts.loader.templateExists(ts.config.Name, ts.config.Type))
8686
}
8787

8888
func TestESLoader_Load(t *testing.T) {
@@ -91,7 +91,7 @@ func TestESLoader_Load(t *testing.T) {
9191
setup := newTestSetup(t, TemplateConfig{Enabled: false})
9292

9393
setup.load(nil)
94-
assert.False(t, setup.loader.templateExists(setup.config.Name))
94+
assert.False(t, setup.loader.templateExists(setup.config.Name, setup.config.Type))
9595
})
9696

9797
t.Run("invalid version", func(t *testing.T) {
@@ -115,14 +115,14 @@ func TestESLoader_Load(t *testing.T) {
115115

116116
t.Run("disabled", func(t *testing.T) {
117117
setup.load(nil)
118-
tmpl := getTemplate(t, setup.client, setup.config.Name)
118+
tmpl := getTemplate(t, setup.client, setup.config.Name, setup.config.Type)
119119
assert.Equal(t, true, tmpl.SourceEnabled())
120120
})
121121

122122
t.Run("enabled", func(t *testing.T) {
123123
setup.config.Overwrite = true
124124
setup.load(nil)
125-
tmpl := getTemplate(t, setup.client, setup.config.Name)
125+
tmpl := getTemplate(t, setup.client, setup.config.Name, setup.config.Type)
126126
assert.Equal(t, false, tmpl.SourceEnabled())
127127
})
128128
})
@@ -140,7 +140,7 @@ func TestESLoader_Load(t *testing.T) {
140140
Name string `config:"name"`
141141
}{Enabled: true, Path: path(t, []string{"testdata", "fields.json"}), Name: nameJSON}
142142
setup.load(nil)
143-
assert.True(t, setup.loader.templateExists(nameJSON))
143+
assert.True(t, setup.loader.templateExists(nameJSON, setup.config.Type))
144144
})
145145

146146
t.Run("load template successful", func(t *testing.T) {
@@ -157,10 +157,19 @@ func TestESLoader_Load(t *testing.T) {
157157
fields: fields,
158158
properties: []string{"foo", "bar"},
159159
},
160+
"default config with fields and component": {
161+
cfg: TemplateConfig{Enabled: true, Type: IndexTemplateComponent},
162+
fields: fields,
163+
properties: []string{"foo", "bar"},
164+
},
160165
"minimal template": {
161166
cfg: TemplateConfig{Enabled: true},
162167
fields: nil,
163168
},
169+
"minimal template component": {
170+
cfg: TemplateConfig{Enabled: true, Type: IndexTemplateComponent},
171+
fields: nil,
172+
},
164173
"fields from file": {
165174
cfg: TemplateConfig{Enabled: true, Fields: path(t, []string{"testdata", "fields.yml"})},
166175
fields: fields,
@@ -181,7 +190,7 @@ func TestESLoader_Load(t *testing.T) {
181190
setup.mustLoad(data.fields)
182191

183192
// Fetch properties
184-
tmpl := getTemplate(t, setup.client, setup.config.Name)
193+
tmpl := getTemplate(t, setup.client, setup.config.Name, setup.config.Type)
185194
val, err := tmpl.GetValue("mappings.properties")
186195
if data.properties == nil {
187196
assert.Error(t, err)
@@ -203,17 +212,17 @@ func TestESLoader_Load(t *testing.T) {
203212
func TestTemplate_LoadFile(t *testing.T) {
204213
setup := newTestSetup(t, TemplateConfig{Enabled: true})
205214
assert.NoError(t, setup.loadFromFile([]string{"..", "fields.yml"}))
206-
assert.True(t, setup.loader.templateExists(setup.config.Name))
215+
assert.True(t, setup.loader.templateExists(setup.config.Name, setup.config.Type))
207216
}
208217

209218
func TestLoadInvalidTemplate(t *testing.T) {
210219
setup := newTestSetup(t, TemplateConfig{})
211220

212221
// Try to load invalid template
213222
template := map[string]interface{}{"json": "invalid"}
214-
err := setup.loader.loadTemplate(setup.config.Name, template)
223+
err := setup.loader.loadTemplate(setup.config.Name, setup.config.Type, template)
215224
assert.Error(t, err)
216-
assert.False(t, setup.loader.templateExists(setup.config.Name))
225+
assert.False(t, setup.loader.templateExists(setup.config.Name, setup.config.Type))
217226
}
218227

219228
// Tests loading the templates for each beat
@@ -225,7 +234,7 @@ func TestLoadBeatsTemplate_fromFile(t *testing.T) {
225234
for _, beat := range beats {
226235
setup := newTestSetup(t, TemplateConfig{Name: beat, Enabled: true})
227236
assert.NoError(t, setup.loadFromFile([]string{"..", "..", beat, "fields.yml"}))
228-
assert.True(t, setup.loader.templateExists(setup.config.Name))
237+
assert.True(t, setup.loader.templateExists(setup.config.Name, setup.config.Type))
229238
}
230239
}
231240

@@ -238,7 +247,7 @@ func TestTemplateSettings(t *testing.T) {
238247
require.NoError(t, setup.loadFromFile([]string{"..", "fields.yml"}))
239248

240249
// Check that it contains the mapping
241-
templateJSON := getTemplate(t, setup.client, setup.config.Name)
250+
templateJSON := getTemplate(t, setup.client, setup.config.Name, setup.config.Type)
242251
assert.Equal(t, 1, templateJSON.NumberOfShards())
243252
assert.Equal(t, false, templateJSON.SourceEnabled())
244253
}
@@ -289,7 +298,7 @@ var dataTests = []struct {
289298
func TestTemplateWithData(t *testing.T) {
290299
setup := newTestSetup(t, TemplateConfig{Enabled: true})
291300
require.NoError(t, setup.loadFromFile([]string{"testdata", "fields.yml"}))
292-
require.True(t, setup.loader.templateExists(setup.config.Name))
301+
require.True(t, setup.loader.templateExists(setup.config.Name, setup.config.Type))
293302
esClient := setup.client.(*eslegclient.Connection)
294303
for _, test := range dataTests {
295304
_, _, err := esClient.Index(setup.config.Name, "_doc", "", nil, test.data)
@@ -302,14 +311,29 @@ func TestTemplateWithData(t *testing.T) {
302311
}
303312
}
304313

305-
func getTemplate(t *testing.T, client ESClient, templateName string) testTemplate {
306-
status, body, err := client.Request("GET", "/_template/"+templateName, "", nil, nil)
314+
func getTemplate(t *testing.T, client ESClient, templateName string, templateType IndexTemplateType) testTemplate {
315+
status, body, err := client.Request("GET", templateLoaderPath[templateType]+templateName, "", nil, nil)
307316
require.NoError(t, err)
308317
require.Equal(t, status, 200)
309318

310319
var response common.MapStr
311320
err = json.Unmarshal(body, &response)
312321
require.NoError(t, err)
322+
require.NotNil(t, response)
323+
324+
if templateType == IndexTemplateComponent {
325+
var tmpl map[string]interface{}
326+
components := response["component_templates"].([]interface{})
327+
for _, ct := range components {
328+
componentTemplate := ct.(map[string]interface{})["component_template"].(map[string]interface{})
329+
tmpl = componentTemplate["template"].(map[string]interface{})
330+
}
331+
return testTemplate{
332+
t: t,
333+
client: client,
334+
MapStr: common.MapStr(tmpl),
335+
}
336+
}
313337

314338
return testTemplate{
315339
t: t,

0 commit comments

Comments
 (0)