Skip to content
Closed
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
16 changes: 16 additions & 0 deletions central/config/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"github.com/stackrox/rox/central/config/datastore"
"github.com/stackrox/rox/central/convert/storagetov1"
"github.com/stackrox/rox/central/convert/v1tostorage"
customMetrics "github.com/stackrox/rox/central/metrics/aggregator"
"github.com/stackrox/rox/central/platform/matcher"
"github.com/stackrox/rox/central/platform/reprocessor"
"github.com/stackrox/rox/central/telemetry/centralclient"
Expand Down Expand Up @@ -131,6 +132,9 @@

// PutConfig updates Central's config
func (s *serviceImpl) PutConfig(ctx context.Context, req *v1.PutConfigRequest) (*storage.Config, error) {

// Validation:

if req.GetConfig() == nil {
return nil, errors.Wrap(errox.InvalidArgs, "config must be specified")
}
Expand Down Expand Up @@ -160,16 +164,28 @@
regexes = append(regexes, regex)
}
}

if err := customMetrics.ParseConfiguration(
req.GetConfig().GetPrivateConfig().GetPrometheusMetricsConfig()); err != nil {
return nil, err
}

Check warning on line 171 in central/config/service/service.go

View check run for this annotation

Codecov / codecov/patch

central/config/service/service.go#L168-L171

Added lines #L168 - L171 were not covered by tests

// Store:

if err := s.datastore.UpsertConfig(ctx, req.GetConfig()); err != nil {
return nil, err
}

// Application:

if req.GetConfig().GetPublicConfig().GetTelemetry().GetEnabled() {
centralclient.Enable()
} else {
centralclient.Disable()
}
matcher.Singleton().SetRegexes(regexes)
go reprocessor.Singleton().RunReprocessor()

Check warning on line 188 in central/config/service/service.go

View check run for this annotation

Codecov / codecov/patch

central/config/service/service.go#L188

Added line #L188 was not covered by tests
return req.GetConfig(), nil
}

Expand Down
108 changes: 108 additions & 0 deletions central/metrics/aggregator/common/condition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package common

import (
"fmt"
"math"
"slices"
"strconv"

"github.com/pkg/errors"
"github.com/stackrox/rox/pkg/glob"
)

type operator string

const (
opZ operator = ""
opEQ operator = "="
)

var knownOperators = []operator{opEQ}

type Condition struct {
op operator
arg string
}

// MakeCondition constructs an condition.
func MakeCondition(op, arg string) (*Condition, error) {
cond := &Condition{operator(op), arg}
if err := cond.validate(); err != nil {
return nil, err
}
return cond, nil
}

// MustMakeCondition constructs an condition and panics on error.
func MustMakeCondition(op, arg string) *Condition {
cond, err := MakeCondition(op, arg)
if err != nil {
panic(err)
}
return cond
}

func (c *Condition) validate() error {
switch {
// Test operator:
case c.op == opZ:
if len(c.arg) > 0 {
return fmt.Errorf("missing operator in %q", c)
}
return errors.New("empty operator")
case !slices.Contains(knownOperators, c.op):
return fmt.Errorf("operator in %q is not one of %q", c, knownOperators)
// Test argument:
case len(c.arg) == 0:
return fmt.Errorf("missing argument in %q", c)
case !c.isFloatArg() && !c.isGlobArg():
return fmt.Errorf("cannot parse the argument in %q", c)
}
return nil
}

func (c *Condition) String() string {
return string(c.op) + c.arg
}

func (c *Condition) isFloatArg() bool {
_, err := strconv.ParseFloat(c.arg, 32)
return err == nil
}

func (c *Condition) isGlobArg() bool {
return glob.Pattern(c.arg).Ptr().Compile() == nil
}

// match returns whether the labels match the condition and the matched label
// value, if matched.
func (c *Condition) match(value string) bool {
if c == nil {
return true
}
if argument, err := strconv.ParseFloat(c.arg, 32); err == nil {
if numericValue, err := strconv.ParseFloat(value, 32); err == nil {
return c.compareFloats(numericValue, argument)
}
}
return c.compareStrings(value, c.arg)
}

func (c *Condition) compareStrings(a, b string) bool {
switch c.op {
case "":
return a != "" && b == ""
case opEQ:
return glob.Pattern(b).Ptr().Match(a)
}
return false

Check warning on line 98 in central/metrics/aggregator/common/condition.go

View check run for this annotation

Codecov / codecov/patch

central/metrics/aggregator/common/condition.go#L98

Added line #L98 was not covered by tests
}

func (c *Condition) compareFloats(a, b float64) bool {
const epsilon = 1e-9
switch c.op {
case opEQ:
return math.Abs(a-b) <= epsilon
}
return false

Check warning on line 107 in central/metrics/aggregator/common/condition.go

View check run for this annotation

Codecov / codecov/patch

central/metrics/aggregator/common/condition.go#L107

Added line #L107 was not covered by tests
}
103 changes: 103 additions & 0 deletions central/metrics/aggregator/common/condition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package common

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_condition_match(t *testing.T) {
labels := map[Label]string{
"string": "value",
"number": "3.4",
"bool": "false",
"empty": "",
}

t.Run("test label and value", func(t *testing.T) {
value := labels["string"]
ok := (&Condition{"=", "value"}).match(value)
assert.True(t, ok)
assert.Equal(t, "value", value)
})

t.Run("test missing label", func(t *testing.T) {
value := labels["nonexistent"]
ok := (&Condition{"=", "value"}).match(value)
assert.Equal(t, "", value)
assert.False(t, ok)
})
t.Run("test empty label value", func(t *testing.T) {
value := labels["empty"]
ok := (&Condition{"=", "value"}).match(value)
assert.Equal(t, "", value)
assert.False(t, ok)

value = labels["label"]
ok = (*Condition)(nil).match(value)
assert.Equal(t, "", value)
assert.True(t, ok)
})
t.Run("test condition with only label", func(t *testing.T) {
value := labels["string"]
ok := (&Condition{"", ""}).match(value)
assert.Equal(t, "value", value)
assert.True(t, ok)
})

type testCase struct {
label Label
cond Condition

match bool
}

for i, c := range []testCase{
{"string", Condition{"=", "value"}, true},
{"string", Condition{"=", "*alu?"}, true},
{"number", Condition{"=", "3.40"}, true},
{"bool", Condition{"=", "false"}, true},

{"string", Condition{"=", "value1"}, false},
{"string", Condition{"=", "*2"}, false},
{"number", Condition{"=", "3.40.1"}, false},
{"bool", Condition{"=", "true"}, false},
} {
value := labels[c.label]
actual := c.cond.match(value)
assert.Equal(t, c.match, actual, "test #%d %s %s", i, c.label, c.cond.String())
}
}

func TestMustMakeCondition(t *testing.T) {
assert.Panics(t, func() { _ = MustMakeCondition("x", "y") })
}

func Test_validate(t *testing.T) {
type testCase struct {
cond Condition
err string
}
cases := []testCase{
// NOK:
{Condition{op: "op"}, `operator in "op" is not one of ["="]`},
{Condition{op: "="}, "missing argument in \"=\""},
{Condition{op: "?", arg: "arg"}, `operator in "?arg" is not one of ["="]`},
{Condition{op: "=", arg: "[a-"}, "cannot parse the argument in \"=[a-\""},
{Condition{arg: "arg"}, "missing operator in \"arg\""},
// OK:
{Condition{}, "empty operator"},
{Condition{op: "=", arg: "arg"}, ""},
{Condition{op: "=", arg: "def"}, ""},
}
for _, c := range cases {
t.Run("cond: "+string(c.cond.op)+c.cond.arg, func(t *testing.T) {
err := c.cond.validate()
if err == nil {
assert.Empty(t, c.err)
} else {
assert.Equal(t, c.err, err.Error())
}
})
}
}
70 changes: 70 additions & 0 deletions central/metrics/aggregator/common/config_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package common

import (
"maps"
"regexp"
"slices"

"github.com/pkg/errors"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/errox"
)

var (
errInvalidConfiguration = errox.InvalidArgs.New("invalid configuration")

// Source: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
metricNamePattern = regexp.MustCompile("^[a-zA-Z_:][a-zA-Z0-9_:]*$")
)

func isKnownLabel(label string, labelOrder map[Label]int) bool {
_, ok := labelOrder[Label(label)]
return ok
}

// validateMetricName ensures the name is alnum_.
func validateMetricName(name string) error {
if len(name) == 0 {
return errors.New("empty")
}
if !metricNamePattern.MatchString(name) {
return errors.New(`doesn't match "` + metricNamePattern.String() + `"`)
}
return nil
}

// ParseMetricLabels converts the storage object to the usable map, validating
// the values.
func ParseMetricLabels(config map[string]*storage.PrometheusMetricsConfig_Labels, labelOrder map[Label]int) (MetricsConfiguration, error) {
result := make(MetricsConfiguration, len(config))
for metric, labels := range config {
if err := validateMetricName(metric); err != nil {
return nil, errInvalidConfiguration.CausedByf(
"invalid metric name %q: %v", metric, err)
}
labelExpression := make(map[Label]Expression, len(labels.GetLabels()))
for label, expression := range labels.GetLabels() {
if !isKnownLabel(label, labelOrder) {
return nil, errInvalidConfiguration.CausedByf(
"label %q for metric %q is not in the list of known labels: %v",
label, metric, slices.Sorted(maps.Keys(labelOrder)))
}

var expr Expression
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why can't we use []*Condition here? It makes the reader to first search what is an Expression to understand what is happening here.

for _, cond := range expression.GetExpression() {
condition, err := MakeCondition(cond.GetOperator(), cond.GetArgument())
if err != nil {
return nil, errInvalidConfiguration.CausedByf(
"failed to parse a condition for metric %q with label %q: %v",
metric, label, err)
}
expr = append(expr, condition)
}
labelExpression[Label(label)] = expr
}
if len(labelExpression) > 0 {
result[MetricName(metric)] = labelExpression
}
}
return result, nil
}
Loading
Loading