Skip to content

Commit 6def317

Browse files
authored
Add initial support to validate the mappings in system tests (#2214)
Add validation of the mappings when running system tests. Currently, this validation does not take into account dynamic templates. This process compares the mappings installed by Fleet (preview mappings) with the ones obtained after ingesting new documents into Elasticsearch. This new validation needs to be enabled by setting a new environment variable "ELASTIC_PACKAGE_FIELD_VALIDATION_TEST_METHOD=mappings". By default, elastic-package keeps using the same mechanism to validate that all fields are documented.
1 parent b9590bd commit 6def317

File tree

13 files changed

+1978
-65
lines changed

13 files changed

+1978
-65
lines changed

.buildkite/pipeline.trigger.integration.tests.sh

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,9 @@ CHECK_PACKAGES_TESTS=(
4444
test-check-packages-benchmarks
4545
test-check-packages-with-logstash
4646
)
47-
for independent_agent in false true ; do
4847
for test in "${CHECK_PACKAGES_TESTS[@]}"; do
49-
label_suffix=""
50-
if [[ "$independent_agent" == "false" ]]; then
51-
label_suffix=" (stack agent)"
52-
fi
5348
test_name=${test#"test-check-packages-"}
54-
echo " - label: \":go: Integration test: ${test_name}${label_suffix}\""
49+
echo " - label: \":go: Integration test: ${test_name}\""
5550
echo " command: ./.buildkite/scripts/integration_tests.sh -t ${test}"
5651
echo " agents:"
5752
echo " provider: \"gcp\""
@@ -64,15 +59,10 @@ for test in "${CHECK_PACKAGES_TESTS[@]}"; do
6459
if [[ $test =~ with-kind$ ]]; then
6560
echo " - build/kubectl-dump.txt"
6661
fi
67-
if [[ "${independent_agent}" == "false" ]]; then
68-
echo " env:"
69-
echo " ELASTIC_PACKAGE_TEST_ENABLE_INDEPENDENT_AGENT: ${independent_agent}"
70-
fi
71-
done
7262
done
7363

7464
pushd test/packages/false_positives > /dev/null
75-
for package in $(find . -maxdepth 1 -mindepth 1 -type d) ; do
65+
while IFS= read -r -d '' package ; do
7666
package_name=$(basename "${package}")
7767
echo " - label: \":go: Integration test (false positive): ${package_name}\""
7868
echo " key: \"integration-false_positives-${package_name}\""
@@ -86,48 +76,56 @@ for package in $(find . -maxdepth 1 -mindepth 1 -type d) ; do
8676
echo " - build/test-results/*.xml"
8777
echo " - build/test-results/*.xml.expected-errors.txt" # these files are uploaded in case it is needed to review the xUnit files in case of CI reports success the step
8878
echo " - build/test-coverage/coverage-*.xml" # these files should not be used to compute the final coverage of elastic-package
89-
done
79+
done < <(find . -maxdepth 1 -mindepth 1 -type d -print0)
9080
popd > /dev/null
9181

9282
pushd test/packages/parallel > /dev/null
93-
for independent_agent in false true; do
94-
for package in $(find . -maxdepth 1 -mindepth 1 -type d) ; do
95-
label_suffix=""
96-
if [[ "$independent_agent" == "false" ]]; then
97-
label_suffix=" (stack agent)"
98-
fi
83+
while IFS= read -r -d '' package ; do
9984
package_name=$(basename "${package}")
100-
if [[ "$independent_agent" == "false" && "$package_name" == "oracle" ]]; then
101-
echoerr "Package \"${package_name}\" skipped: not supported with Elastic Agent running in the stack (missing required software)."
102-
continue
103-
fi
104-
105-
if [[ "$independent_agent" == "false" && "$package_name" == "auditd_manager" ]]; then
106-
echoerr "Package \"${package_name}\" skipped: not supported with Elastic Agent running in the stack (missing capabilities)."
107-
continue
108-
fi
85+
echo " - label: \":go: Integration test: ${package_name}\""
86+
echo " key: \"integration-parallel-${package_name}-agent\""
87+
echo " command: ./.buildkite/scripts/integration_tests.sh -t test-check-packages-parallel -p ${package_name}"
88+
echo " env:"
89+
echo " UPLOAD_SAFE_LOGS: 1"
90+
echo " agents:"
91+
echo " provider: \"gcp\""
92+
echo " image: \"${UBUNTU_X86_64_AGENT_IMAGE}\""
93+
echo " artifact_paths:"
94+
echo " - build/test-results/*.xml"
95+
echo " - build/test-coverage/coverage-*.xml" # these files should not be used to compute the final coverage of elastic-package
96+
done < <(find . -maxdepth 1 -mindepth 1 -type d -print0)
10997

110-
if [[ "$independent_agent" == "false" && "$package_name" == "custom_entrypoint" ]]; then
111-
echoerr "Package \"${package_name}\" skipped: not supported with Elastic Agent running in the stack (missing required files deployed in provisioning)."
112-
continue
113-
fi
98+
# Run system tests with the Elastic Agent from the Elastic stack just for one package
99+
package_name="apache"
100+
echo " - label: \":go: Integration test: ${package_name} (stack agent)\""
101+
echo " key: \"integration-parallel-${package_name}-stack-agent\""
102+
echo " command: ./.buildkite/scripts/integration_tests.sh -t test-check-packages-parallel -p ${package_name}"
103+
echo " env:"
104+
echo " UPLOAD_SAFE_LOGS: 1"
105+
echo " ELASTIC_PACKAGE_TEST_ENABLE_INDEPENDENT_AGENT: false"
106+
echo " agents:"
107+
echo " provider: \"gcp\""
108+
echo " image: \"${UBUNTU_X86_64_AGENT_IMAGE}\""
109+
echo " artifact_paths:"
110+
echo " - build/test-results/*.xml"
111+
echo " - build/test-coverage/coverage-*.xml" # these files should not be used to compute the final coverage of elastic-package
114112

115-
echo " - label: \":go: Integration test: ${package_name}${label_suffix}\""
116-
echo " key: \"integration-parallel-${package_name}-agent-${independent_agent}\""
113+
# Add steps to test validation method mappings
114+
while IFS= read -r -d '' package ; do
115+
package_name=$(basename "${package}")
116+
echo " - label: \":go: Integration test: ${package_name} (just validate mappings)\""
117+
echo " key: \"integration-parallel-${package_name}-agent-validate-mappings\""
117118
echo " command: ./.buildkite/scripts/integration_tests.sh -t test-check-packages-parallel -p ${package_name}"
118119
echo " env:"
119120
echo " UPLOAD_SAFE_LOGS: 1"
120-
if [[ "${independent_agent}" == "false" ]]; then
121-
echo " ELASTIC_PACKAGE_TEST_ENABLE_INDEPENDENT_AGENT: ${independent_agent}"
122-
fi
121+
echo " ELASTIC_PACKAGE_FIELD_VALIDATION_TEST_METHOD: mappings"
123122
echo " agents:"
124123
echo " provider: \"gcp\""
125124
echo " image: \"${UBUNTU_X86_64_AGENT_IMAGE}\""
126125
echo " artifact_paths:"
127126
echo " - build/test-results/*.xml"
128127
echo " - build/test-coverage/coverage-*.xml" # these files should not be used to compute the final coverage of elastic-package
129-
done
130-
done
128+
done < <(find . -maxdepth 1 -mindepth 1 -type d -print0)
131129
popd > /dev/null
132130

133131
# TODO: Missing docker & docker-compose in MACOS ARM agent image, skip installation of packages in the meantime.
@@ -166,19 +164,11 @@ echo " image: \"${UBUNTU_X86_64_AGENT_IMAGE}\""
166164
echo " artifact_paths:"
167165
echo " - build/elastic-stack-dump/install-zip-shellinit/logs/*.log"
168166

169-
for independent_agent in false true; do
170-
label_suffix=""
171-
if [[ "$independent_agent" == "false" ]]; then
172-
label_suffix=" (stack agent)"
173-
fi
174-
echo " - label: \":go: Integration test: system-flags${label_suffix}\""
175-
echo " command: ./.buildkite/scripts/integration_tests.sh -t test-system-test-flags"
176-
echo " agents:"
177-
echo " provider: \"gcp\""
178-
echo " image: \"${UBUNTU_X86_64_AGENT_IMAGE}\""
179-
echo " env:"
180-
echo " ELASTIC_PACKAGE_TEST_ENABLE_INDEPENDENT_AGENT: ${independent_agent}"
181-
done
167+
echo " - label: \":go: Integration test: system-flags\""
168+
echo " command: ./.buildkite/scripts/integration_tests.sh -t test-system-test-flags"
169+
echo " agents:"
170+
echo " provider: \"gcp\""
171+
echo " image: \"${UBUNTU_X86_64_AGENT_IMAGE}\""
182172

183173
echo " - label: \":go: Integration test: profiles-command\""
184174
echo " command: ./.buildkite/scripts/integration_tests.sh -t test-profiles-command"

.buildkite/scripts/integration_tests.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ if [[ "${TARGET}" == "${PARALLEL_TARGET}" ]] || [[ "${TARGET}" == "${FALSE_POSIT
122122
package_folder="${package_folder}-stack_agent"
123123
fi
124124

125+
if [[ "${ELASTIC_PACKAGE_FIELD_VALIDATION_TEST_METHOD:-""}" != "" ]]; then
126+
package_folder="${package_folder}-${ELASTIC_PACKAGE_FIELD_VALIDATION_TEST_METHOD}"
127+
fi
128+
125129
if [[ "${retry_count}" -ne 0 ]]; then
126130
package_folder="${package_folder}_retry_${retry_count}"
127131
fi

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,9 +677,16 @@ There are available some environment variables that could be used to change some
677677
- `ELASTIC_PACKAGE_DISABLE_ELASTIC_AGENT_WOLFI`: If set to `true`, the Elastic Agent image used for running agents will be using the Ubuntu docker images
678678
(e.g. `docker.elastic.co/elastic-agent/elastic-agent-complete`). If set to `false`, the Elastic Agent image used for the running agents will be based on the wolfi
679679
images (e.g. `docker.elastic.co/elastic-agent/elastic-agent-wolfi`). Default: `false`.
680-
- `ELASTIC_PACKAGE_TEST_DUMP_SCENARIO_DOCS. If the variable is set, elastic-package will dump to a file the documents generated
680+
- `ELASTIC_PACKAGE_TEST_DUMP_SCENARIO_DOCS`. If the variable is set, elastic-package will dump to a file the documents generated
681681
by system tests before they are verified. This is useful to know exactly what fields are being verified when investigating
682682
issues on this step. Documents are dumped to a file in the system temporary directory. It is disabled by default.
683+
- `ELASTIC_PACKAGE_TEST_ENABLE_INDEPENDENT_AGENT`. If the variable is set to false, all system tests defined in the package will use
684+
the Elastic Agent started along with the stack. If set to true, a new Elastic Agent will be started and enrolled for each test defined in the
685+
package (and unenrolled at the end of each test). Default: `true`.
686+
- `ELASTIC_PACKAGE_FIELD_VALIDATION_TEST_METHOD`. This variable can take one of these values: `all`, `mappings` or `fields`. If this
687+
variable is set to `fields`, then validation of fields will be based on the documents ingested into Elasticsearch. If this is set to
688+
`mappings`, then validation of fields will be based on the mappings generated when the documents are ingested into Elasticsearch. If
689+
set to `all`, then validation will be based on both methods mentioned previously. Default option: `fields`.
683690

684691
- To configure the Elastic stack to be used by `elastic-package`:
685692
- `ELASTIC_PACKAGE_ELASTICSEARCH_HOST`: Host of the elasticsearch (e.g. https://127.0.0.1:9200)

internal/elasticsearch/client.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,85 @@ func (client *Client) redHealthCause(ctx context.Context) (string, error) {
279279
}
280280
return strings.Join(causes, ", "), nil
281281
}
282+
283+
func (c *Client) SimulateIndexTemplate(ctx context.Context, indexTemplateName string) (json.RawMessage, json.RawMessage, error) {
284+
resp, err := c.Indices.SimulateTemplate(
285+
c.Indices.SimulateTemplate.WithContext(ctx),
286+
c.Indices.SimulateTemplate.WithName(indexTemplateName),
287+
)
288+
if err != nil {
289+
return nil, nil, fmt.Errorf("failed to get field mapping for data stream %q: %w", indexTemplateName, err)
290+
}
291+
defer resp.Body.Close()
292+
if resp.IsError() {
293+
return nil, nil, fmt.Errorf("error getting mapping: %s", resp)
294+
}
295+
body, err := io.ReadAll(resp.Body)
296+
if err != nil {
297+
return nil, nil, fmt.Errorf("error reading mapping body: %w", err)
298+
}
299+
300+
type mappingsIndexTemplate struct {
301+
DynamicTemplates json.RawMessage `json:"dynamic_templates"`
302+
Properties json.RawMessage `json:"properties"`
303+
}
304+
305+
type indexTemplateSimulated struct {
306+
// Settings json.RawMessage `json:"settings"`
307+
Mappings mappingsIndexTemplate `json:"mappings"`
308+
}
309+
310+
type previewTemplate struct {
311+
Template indexTemplateSimulated `json:"template"`
312+
}
313+
314+
var preview previewTemplate
315+
316+
if err := json.Unmarshal(body, &preview); err != nil {
317+
return nil, nil, fmt.Errorf("error unmarshaling mappings: %w", err)
318+
}
319+
320+
return preview.Template.Mappings.DynamicTemplates, preview.Template.Mappings.Properties, nil
321+
}
322+
323+
func (c *Client) DataStreamMappings(ctx context.Context, dataStreamName string) (json.RawMessage, json.RawMessage, error) {
324+
mappingResp, err := c.Indices.GetMapping(
325+
c.Indices.GetMapping.WithContext(ctx),
326+
c.Indices.GetMapping.WithIndex(dataStreamName),
327+
)
328+
if err != nil {
329+
return nil, nil, fmt.Errorf("failed to get field mapping for data stream %q: %w", dataStreamName, err)
330+
}
331+
defer mappingResp.Body.Close()
332+
if mappingResp.IsError() {
333+
return nil, nil, fmt.Errorf("error getting mapping: %s", mappingResp)
334+
}
335+
body, err := io.ReadAll(mappingResp.Body)
336+
if err != nil {
337+
return nil, nil, fmt.Errorf("error reading mapping body: %w", err)
338+
}
339+
340+
type mappings struct {
341+
DynamicTemplates json.RawMessage `json:"dynamic_templates"`
342+
Properties json.RawMessage `json:"properties"`
343+
}
344+
345+
mappingsRaw := map[string]struct {
346+
Mappings mappings `json:"mappings"`
347+
}{}
348+
349+
if err := json.Unmarshal(body, &mappingsRaw); err != nil {
350+
return nil, nil, fmt.Errorf("error unmarshaling mappings: %w", err)
351+
}
352+
353+
if len(mappingsRaw) != 1 {
354+
return nil, nil, fmt.Errorf("exactly 1 mapping was expected, got %d", len(mappingsRaw))
355+
}
356+
357+
var mappingsDefinition mappings
358+
for _, v := range mappingsRaw {
359+
mappingsDefinition = v.Mappings
360+
}
361+
362+
return mappingsDefinition.DynamicTemplates, mappingsDefinition.Properties, nil
363+
}

internal/fields/dependency_manager.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,31 @@ func parseECSFieldsSchema(content []byte) ([]FieldDefinition, error) {
134134
return nil, fmt.Errorf("unmarshalling field body failed: %w", err)
135135
}
136136

137+
// Force to set External as "ecs" in all these fields to be able to distinguish
138+
// which fields come from ECS and which ones are loaded from the package directory
139+
fields = setExternalAsECS(fields)
140+
137141
return fields, nil
138142
}
139143

144+
func setExternalAsECS(fields []FieldDefinition) []FieldDefinition {
145+
for i := 0; i < len(fields); i++ {
146+
f := &fields[i]
147+
f.External = "ecs"
148+
if len(f.MultiFields) > 0 {
149+
for j := 0; j < len(f.MultiFields); j++ {
150+
mf := &f.MultiFields[j]
151+
mf.External = "ecs"
152+
}
153+
}
154+
if len(f.Fields) > 0 {
155+
f.Fields = setExternalAsECS(f.Fields)
156+
}
157+
}
158+
159+
return fields
160+
}
161+
140162
func asGitReference(reference string) (string, error) {
141163
if !strings.HasPrefix(reference, gitReferencePrefix) {
142164
return "", errors.New(`invalid Git reference ("git@" prefix expected)`)

internal/fields/dependency_manager_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,3 +785,55 @@ func TestDependencyManagerWithECS(t *testing.T) {
785785
})
786786
}
787787
}
788+
789+
func TestValidate_SetExternalECS(t *testing.T) {
790+
finder := packageRootTestFinder{"../../test/packages/other/imported_mappings_tests"}
791+
792+
validator, err := createValidatorForDirectoryAndPackageRoot("../../test/packages/other/imported_mappings_tests/data_stream/first",
793+
finder,
794+
WithSpecVersion("2.3.0"),
795+
WithEnabledImportAllECSSChema(true))
796+
require.NoError(t, err)
797+
require.NotNil(t, validator)
798+
799+
require.NotEmpty(t, validator.Schema)
800+
801+
cases := []struct {
802+
title string
803+
field string
804+
external string
805+
exists bool
806+
}{
807+
{
808+
title: "field defined just in ECS",
809+
field: "ecs.version",
810+
external: "ecs",
811+
exists: true,
812+
},
813+
{
814+
title: "field defined fields directory package",
815+
field: "service.status.duration.histogram",
816+
external: "",
817+
exists: true,
818+
},
819+
{
820+
title: "undefined field",
821+
field: "foo",
822+
external: "",
823+
exists: false,
824+
},
825+
}
826+
827+
for _, c := range cases {
828+
t.Run(c.title, func(t *testing.T) {
829+
found := FindElementDefinition(c.field, validator.Schema)
830+
if !c.exists {
831+
assert.Nil(t, found)
832+
return
833+
}
834+
835+
require.NotNil(t, found)
836+
assert.Equal(t, c.external, found.External)
837+
})
838+
}
839+
}

0 commit comments

Comments
 (0)