Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compliance tests for 3.1.0 #709

Merged
merged 4 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .buildkite/pipeline.trigger.compliance.tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ steps:
EOF

# Generate each test we want to do.
compliance_test 8.10.0-SNAPSHOT 2.10.0
compliance_test 8.13.0-SNAPSHOT 3.1.0
compliance_test 8.12.0 3.0.4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be used a non released spec version here too ? Would it make sense ?

I was thinking of adding here also the current 3.0.5-next version as a version to comply too (so we have the stable and the next one).

Suggested change
compliance_test 8.12.0 3.0.4
compliance_test 8.12.0 3.0.5-next
compliance_test 8.12.0 3.0.4

Or just use compliance_test 8.12.0 3.0.5-next ?
It's also true that there is no test package with version 3.0.* currently

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Umm, actually I did some changes to be able to test with unreleased versions, in principle we could have tested 3.0.5 without the next suffix, as is done with 3.1.0.

compliance_test 8.9.0 2.7.0

# Annotate junit results.
Expand Down
23 changes: 11 additions & 12 deletions compliance/compliance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"testing"

"github.com/cucumber/godog"
"github.com/elastic/go-elasticsearch/v8/typedapi/types"
"golang.org/x/exp/slices"
)

Expand Down Expand Up @@ -68,19 +68,15 @@ func indexTemplateHasAFieldWith(indexTemplateName, fieldName, condition string)
return err
}

// TODO: Properly build conditions.
switch condition {
case "runtime:true":
if _, isRuntime := fieldMapping.(types.RuntimeField); isRuntime {
return nil
}
if fieldMapping.CheckCondition(condition) {
return nil
}

d, err := json.MarshalIndent(fieldMapping, "", " ")
if err != nil {
return err
}
fmt.Printf("Found the following mapping of type %T for field %q:\n", fieldMapping, fieldName)
fmt.Printf("Found the following mapping for field %q:\n", fieldName)
fmt.Println(string(d))
return fmt.Errorf("conditon %q not satisfied by field %q", condition, fieldName)
}
Expand Down Expand Up @@ -162,13 +158,16 @@ func thereIsATransform(transformID string) error {
return err
}

resp, err := es.client.Transform.GetTransform().
TransformId(transformID).
Do(context.TODO())
resp, err := es.client.TransformGetTransform(
es.client.TransformGetTransform.WithContext(context.TODO()),
es.client.TransformGetTransform.WithTransformID(transformID),
)
if err != nil {
return fmt.Errorf("failed to get transform %q: %w", transformID, err)
}
if resp.Count == 0 {
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("transform %q not found", transformID)
}

Expand Down
64 changes: 55 additions & 9 deletions compliance/elasticsearch.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"

"github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/typedapi/types"
)

// Elasticsearch is an elasticsearch client.
type Elasticsearch struct {
client *elasticsearch.TypedClient
client *elasticsearch.Client
}

// NewElasticsearchClient creates a new Elasticsearch client.
Expand Down Expand Up @@ -49,7 +51,7 @@ func NewElasticsearchClient() (*Elasticsearch, error) {
},
}
}
client, err := elasticsearch.NewTypedClient(config)
client, err := elasticsearch.NewClient(config)
if err != nil {
return nil, err
}
Expand All @@ -60,25 +62,69 @@ func NewElasticsearchClient() (*Elasticsearch, error) {
}

// IndexTemplate looks for an index template.
func (es *Elasticsearch) IndexTemplate(name string) (*types.IndexTemplate, error) {
resp, err := es.client.Indices.GetIndexTemplate().Name(name).Do(context.TODO())
func (es *Elasticsearch) IndexTemplate(name string) (*IndexTemplate, error) {
resp, err := es.client.Indices.GetIndexTemplate(
es.client.Indices.GetIndexTemplate.WithContext(context.TODO()),
es.client.Indices.GetIndexTemplate.WithName(name),
)
if err != nil {
return nil, err
}
if n := len(resp.IndexTemplates); n != 1 {
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP status code %d", resp.StatusCode)
}

var templatesResponse struct {
IndexTemplates []struct {
IndexTemplate *IndexTemplate `json:"index_template"`
} `json:"index_templates"`
}
err = newJSONDecoder(resp.Body).Decode(&templatesResponse)
if err != nil {
return nil, err
}

if n := len(templatesResponse.IndexTemplates); n != 1 {
return nil, fmt.Errorf("one index template expected, found %d", n)
}

return &resp.IndexTemplates[0].IndexTemplate, nil
return templatesResponse.IndexTemplates[0].IndexTemplate, nil
}

// SimulateIndexTemplate simulates the instantiation of an index template, resolving its
// component templates.
func (es *Elasticsearch) SimulateIndexTemplate(name string) (*SimulatedIndexTemplate, error) {
resp, err := es.client.Indices.SimulateTemplate().Name(name).Do(context.TODO())
resp, err := es.client.Indices.SimulateTemplate(
es.client.Indices.SimulateTemplate.WithName(name),
es.client.Indices.SimulateTemplate.WithContext(context.TODO()),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP status code %d", resp.StatusCode)
}

var simulateResponse struct {
Template *SimulatedIndexTemplate `json:"template"`
}
err = newJSONDecoder(resp.Body).Decode(&simulateResponse)
if err != nil {
return nil, err
}
if simulateResponse.Template == nil {
return nil, errors.New("empty template simulated, something is wrong")
}

return simulateResponse.Template, nil
}

return &SimulatedIndexTemplate{resp.Template}, nil
func newJSONDecoder(r io.Reader) *json.Decoder {
dec := json.NewDecoder(r)
dec.UseNumber()
return dec
}
8 changes: 8 additions & 0 deletions compliance/features/field-types.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Feature: Field types
Check support for field types that have been added since Fleet exists.

@3.1.0
Scenario: Package uses the "counted_keyword" type
Given the "counted_keyword" package is installed
And a policy is created with "counted_keyword" package
Then index template "metrics-counted_keyword.foo" has a field "foo.count" with "type:counted_keyword"
8 changes: 8 additions & 0 deletions compliance/features/subobjects.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Feature: Subobjects
Support to disable subobjects in object fields.

@3.1.0
Scenario: Installer leverages subobjects false
Given the "subobjects_false" package is installed
And a policy is created with "subobjects_false" package
Then index template "metrics-subobjects_false.foo" has a field "foo.object" with "subobjects:false"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main reason to stop using the typed API, with this one there is no way to check if a property has the subobjects property:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to be able to use the typed API, but it's true that not using it give us more flexibility.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the typed API is quite nice, but limited for some things, and complicated for dynamic objects in responses.

98 changes: 80 additions & 18 deletions compliance/indextemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,94 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"strings"

"github.com/elastic/go-elasticsearch/v8/typedapi/types"
)

// IndexTemplate contains the result of getting an index template, decoded
// from json into a map.
type IndexTemplate struct {
IndexPatterns []string `json:"index_patterns"`
}

// SimulatedIndexTemplate contains the result of simulating an index template,
// with the component templates resolved.
type SimulatedIndexTemplate struct {
types.Template
Mappings struct {
Runtime map[string]MappingProperty `json:"runtime"`
Properties map[string]MappingProperty `json:"properties"`
} `json:"mappings"`
}

// MappingProperty is the definition of a property in an index template.
type MappingProperty map[string]any

// CheckCondition checks if a property satisfies a condition. Conditions are in the
// form key:value, where the key and the value are compared with attributes of the
// property.
func (p MappingProperty) CheckCondition(condition string) bool {
key, value, ok := strings.Cut(condition, ":")
if !ok {
panic("cannot understand condition " + condition)
}

v, ok := p[strings.TrimSpace(key)]
if !ok {
return false
}

switch v := v.(type) {
case string:
return strings.TrimSpace(value) == strings.TrimSpace(v)
case bool:
expected, err := strconv.ParseBool(value)
return err != nil || expected == v
case json.Number:
expected, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return false
}
n, err := v.Int64()
if err != nil {
return false
}
return expected == n
}

return false
}

// Properties returns the child properties of this property.
func (p MappingProperty) Properties() (map[string]MappingProperty, error) {
properties, ok := p["properties"]
if !ok {
return nil, nil
}
mapProperties, ok := properties.(map[string]any)
if !ok {
return nil, errors.New("not a map")
}

result := make(map[string]MappingProperty)
for k, v := range mapProperties {
anyMap, ok := v.(map[string]any)
if !ok {
return nil, errors.New("not a map")
}
result[k] = MappingProperty(anyMap)
}

return result, nil
}

// FieldMapping looks for the definition of a field in the simulated index template.
func (t *SimulatedIndexTemplate) FieldMapping(name string) (any, error) {
func (t *SimulatedIndexTemplate) FieldMapping(name string) (MappingProperty, error) {
if runtimeField, isRuntime := t.Mappings.Runtime[name]; isRuntime {
// TODO: Look for some solution to don't need to modify the properties.
runtimeField["runtime"] = true
return runtimeField, nil
}

Expand All @@ -35,19 +107,9 @@ func (t *SimulatedIndexTemplate) FieldMapping(name string) (any, error) {
return property, nil
}

// Using reflect because Property type is defined as any and there are too many
// possible matching types.
value := reflect.ValueOf(property).Elem()
if value.Kind() != reflect.Struct {
return nil, fmt.Errorf("property %q not found in index template, unexpected parent kind %s", name, value.Kind())
}
nextPropertiesValue := value.FieldByName("Properties")
if nextPropertiesValue.IsZero() {
return nil, fmt.Errorf("property %q not found in index template, zero properties", name)
}
nextProperties, ok := nextPropertiesValue.Interface().(map[string]types.Property)
if !ok {
return nil, fmt.Errorf("property %q not found in index template, cannot convert properties", name)
Copy link
Member Author

@jsoriano jsoriano Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another problem of the typed API is that we ended up relying on reflection for some things.

nextProperties, err := property.Properties()
if err != nil {
return nil, fmt.Errorf("property %q not found in index template: %w", name, err)
}
properties = nextProperties
}
Expand Down
Loading