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

Improve config validation for prometheus receiver and target allocator #1729

Merged
merged 6 commits into from
May 24, 2023
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
16 changes: 16 additions & 0 deletions .chloggen/fix_validate-ta-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. operator, target allocator, github action)
component: operator

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Improve config validation for prometheus receiver and target allocator

# One or more tracking issues related to the change
issues: [1581]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
7 changes: 6 additions & 1 deletion apis/v1alpha1/opentelemetrycollector_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"

"github.com/open-telemetry/opentelemetry-operator/pkg/featuregate"
ta "github.com/open-telemetry/opentelemetry-operator/pkg/targetallocator/adapters"
)

Expand Down Expand Up @@ -160,7 +161,11 @@ func (r *OpenTelemetryCollector) validateCRDSpec() error {

// validate Prometheus config for target allocation
if r.Spec.TargetAllocator.Enabled {
_, err := ta.ConfigToPromConfig(r.Spec.Config)
promCfg, err := ta.ConfigToPromConfig(r.Spec.Config)
if err != nil {
return fmt.Errorf("the OpenTelemetry Spec Prometheus configuration is incorrect, %w", err)
}
err = ta.ValidatePromConfig(promCfg, r.Spec.TargetAllocator.Enabled, featuregate.EnableTargetAllocatorRewrite.IsEnabled())
if err != nil {
return fmt.Errorf("the OpenTelemetry Spec Prometheus configuration is incorrect, %w", err)
}
Expand Down
9 changes: 9 additions & 0 deletions cmd/otel-allocator/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,12 @@ func ParseCLI() (CLIConfig, error) {
cLIConf.ClusterConfig = clusterConfig
return cLIConf, nil
}

// ValidateConfig validates the cli and file configs together.
func ValidateConfig(config *Config, cliConfig *CLIConfig) error {
scrapeConfigsPresent := (config.Config != nil && len(config.Config.ScrapeConfigs) > 0)
if !(*cliConfig.PromCRWatcherConf.Enabled || scrapeConfigsPresent) {
return fmt.Errorf("at least one scrape config must be defined, or Prometheus CR watching must be enabled")
}
return nil
}
61 changes: 61 additions & 0 deletions cmd/otel-allocator/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ func TestLoad(t *testing.T) {
},
wantErr: assert.NoError,
},
{
name: "no config",
args: args{
file: "./testdata/no_config.yaml",
},
want: Config{},
wantErr: assert.NoError,
},
{
name: "service monitor pod monitor selector",
args: args{
Expand Down Expand Up @@ -157,3 +165,56 @@ func TestLoad(t *testing.T) {
})
}
}

func TestValidateConfig(t *testing.T) {
enabled := true
disabled := false
testCases := []struct {
name string
cliConfig CLIConfig
fileConfig Config
expectedErr error
}{
{
name: "promCR enabled, no Prometheus config",
cliConfig: CLIConfig{PromCRWatcherConf: PrometheusCRWatcherConfig{Enabled: &enabled}},
fileConfig: Config{Config: nil},
expectedErr: nil,
},
{
name: "promCR disabled, no Prometheus config",
cliConfig: CLIConfig{PromCRWatcherConf: PrometheusCRWatcherConfig{Enabled: &disabled}},
fileConfig: Config{Config: nil},
expectedErr: fmt.Errorf("at least one scrape config must be defined, or Prometheus CR watching must be enabled"),
},
{
name: "promCR disabled, Prometheus config present, no scrapeConfigs",
cliConfig: CLIConfig{PromCRWatcherConf: PrometheusCRWatcherConfig{Enabled: &disabled}},
fileConfig: Config{Config: &promconfig.Config{}},
expectedErr: fmt.Errorf("at least one scrape config must be defined, or Prometheus CR watching must be enabled"),
},
{
name: "promCR disabled, Prometheus config present, scrapeConfigs present",
cliConfig: CLIConfig{PromCRWatcherConf: PrometheusCRWatcherConfig{Enabled: &disabled}},
fileConfig: Config{
Config: &promconfig.Config{ScrapeConfigs: []*promconfig.ScrapeConfig{{}}},
},
expectedErr: nil,
},
{
name: "promCR enabled, Prometheus config present, scrapeConfigs present",
cliConfig: CLIConfig{PromCRWatcherConf: PrometheusCRWatcherConfig{Enabled: &enabled}},
fileConfig: Config{
Config: &promconfig.Config{ScrapeConfigs: []*promconfig.ScrapeConfig{{}}},
},
expectedErr: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateConfig(&tc.fileConfig, &tc.cliConfig)
assert.Equal(t, tc.expectedErr, err)
})
}
}
Empty file.
4 changes: 4 additions & 0 deletions cmd/otel-allocator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ func main() {
setupLog.Error(configLoadErr, "Unable to load configuration")
}

if validationErr := config.ValidateConfig(&cfg, &cliConf); validationErr != nil {
setupLog.Error(validationErr, "Invalid configuration")
Copy link
Contributor

Choose a reason for hiding this comment

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

in a future version should we fast fail here?

}

cliConf.RootLogger.Info("Starting the Target Allocator")
ctx := context.Background()
log := ctrl.Log.WithName("allocator")
Expand Down
4 changes: 4 additions & 0 deletions cmd/otel-allocator/target/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ func NewDiscoverer(log logr.Logger, manager *discovery.Manager, hook discoveryHo
}

func (m *Discoverer) ApplyConfig(source allocatorWatcher.EventSource, cfg *config.Config) error {
if cfg == nil {
m.log.Info("Service Discovery got empty Prometheus config", "source", source.String())
return nil
jaronoff97 marked this conversation as resolved.
Show resolved Hide resolved
}
m.configsMap[source] = cfg
jobToScrapeConfig := make(map[string]*config.ScrapeConfig)

Expand Down
17 changes: 17 additions & 0 deletions cmd/otel-allocator/target/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,23 @@ func TestDiscovery_ScrapeConfigHashing(t *testing.T) {
}
}

func TestDiscovery_NoConfig(t *testing.T) {
scu := &mockScrapeConfigUpdater{mockCfg: map[string]*promconfig.ScrapeConfig{}}
ctx, cancelFunc := context.WithCancel(context.Background())
d := discovery.NewManager(ctx, gokitlog.NewNopLogger())
manager := NewDiscoverer(ctrl.Log.WithName("test"), d, nil, scu)
defer close(manager.close)
defer cancelFunc()

go func() {
err := d.Run()
assert.NoError(t, err)
}()
// check the updated scrape configs
expectedScrapeConfigs := map[string]*promconfig.ScrapeConfig{}
assert.Equal(t, expectedScrapeConfigs, scu.mockCfg)
}

func BenchmarkApplyScrapeConfig(b *testing.B) {
numConfigs := 1000
scrapeConfig := promconfig.ScrapeConfig{
Expand Down
5 changes: 5 additions & 0 deletions pkg/collector/reconcile/config_replace.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ func ReplaceConfig(instance v1alpha1.OpenTelemetryCollector) (string, error) {
return "", getCfgPromErr
}

validateCfgPromErr := ta.ValidatePromConfig(promCfgMap, instance.Spec.TargetAllocator.Enabled, featuregate.EnableTargetAllocatorRewrite.IsEnabled())
Copy link
Contributor

Choose a reason for hiding this comment

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

Is ValidatePromConfig() meant to be called every time ta.ConfigToPromConfig() is called? Put another way, is there ever a situation when we want to call one function without the other? I'm wondering the benefit of two separate functions. Is it separation of concerns? Not making a breaking change to the adapters pkg API?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not making a breaking change and keeping current semantics of ta.ConfigToPromConfig() is part of it, although the separation of concerns is a nice bonus from that. Though overall there isn't any strong reason behind it, if we're ok breaking ConfigToPromConfig, we could simply make validation a private function and have ConfigToPromConfig call it.

Copy link
Contributor

Choose a reason for hiding this comment

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

that sounds like a good followup for the future :)

if validateCfgPromErr != nil {
return "", validateCfgPromErr
}

// yaml marshaling/unsmarshaling is preferred because of the problems associated with the conversion of map to a struct using mapstructure
promCfg, marshalErr := yaml.Marshal(promCfgMap)
if marshalErr != nil {
Expand Down
9 changes: 6 additions & 3 deletions pkg/collector/reconcile/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func desiredTAConfigMap(params Params) (corev1.ConfigMap, error) {
labels["app.kubernetes.io/version"] = "latest"
}

promConfig, err := ta.ConfigToPromConfig(params.Instance.Spec.Config)
prometheusReceiverConfig, err := ta.ConfigToPromConfig(params.Instance.Spec.Config)
if err != nil {
return corev1.ConfigMap{}, err
}
Expand All @@ -114,8 +114,11 @@ func desiredTAConfigMap(params Params) (corev1.ConfigMap, error) {
"app.kubernetes.io/managed-by": "opentelemetry-operator",
"app.kubernetes.io/component": "opentelemetry-collector",
}
// We only take the "config" from the returned object, we don't need the "target_allocator" configuration here.
taConfig["config"] = promConfig["config"]
// We only take the "config" from the returned object, if it's present
if prometheusConfig, ok := prometheusReceiverConfig["config"]; ok {
taConfig["config"] = prometheusConfig
}

if len(params.Instance.Spec.TargetAllocator.AllocationStrategy) > 0 {
taConfig["allocation_strategy"] = params.Instance.Spec.TargetAllocator.AllocationStrategy
} else {
Expand Down
26 changes: 26 additions & 0 deletions pkg/targetallocator/adapters/config_to_prom_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package adapters

import (
"errors"
"fmt"

"github.com/open-telemetry/opentelemetry-operator/pkg/collector/adapters"
Expand Down Expand Up @@ -57,3 +58,28 @@ func ConfigToPromConfig(cfg string) (map[interface{}]interface{}, error) {

return prometheus, nil
}

// ValidatePromConfig checks if the prometheus receiver config is valid given other collector-level settings.
func ValidatePromConfig(config map[interface{}]interface{}, targetAllocatorEnabled bool, targetAllocatorRewriteEnabled bool) error {
_, promConfigExists := config["config"]

if targetAllocatorEnabled {
if targetAllocatorRewriteEnabled { // if rewrite is enabled, we will add a target_allocator section during rewrite
return nil
}
_, targetAllocatorExists := config["target_allocator"]

// otherwise, either the target_allocator or config section needs to be here
if !(promConfigExists || targetAllocatorExists) {
return errors.New("either target allocator or prometheus config needs to be present")
}

return nil
}
// if target allocator isn't enabled, we need a config section
if !promConfigExists {
return errorNoComponent("prometheusConfig")
}

return nil
}
68 changes: 68 additions & 0 deletions pkg/targetallocator/adapters/config_to_prom_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package adapters_test

import (
"errors"
"fmt"
"reflect"
"testing"
Expand Down Expand Up @@ -114,3 +115,70 @@ func TestExtractPromConfigFromNullConfig(t *testing.T) {
// verify
assert.True(t, reflect.ValueOf(promConfig).IsNil())
}

func TestValidatePromConfig(t *testing.T) {
testCases := []struct {
description string
config map[interface{}]interface{}
targetAllocatorEnabled bool
targetAllocatorRewriteEnabled bool
expectedError error
}{
{
description: "target_allocator and rewrite enabled",
config: map[interface{}]interface{}{},
targetAllocatorEnabled: true,
targetAllocatorRewriteEnabled: true,
expectedError: nil,
},
{
description: "target_allocator enabled, target_allocator section present",
config: map[interface{}]interface{}{
"target_allocator": map[interface{}]interface{}{},
},
targetAllocatorEnabled: true,
targetAllocatorRewriteEnabled: false,
expectedError: nil,
},
{
description: "target_allocator enabled, config section present",
config: map[interface{}]interface{}{
"config": map[interface{}]interface{}{},
},
targetAllocatorEnabled: true,
targetAllocatorRewriteEnabled: false,
expectedError: nil,
},
{
description: "target_allocator enabled, neither section present",
config: map[interface{}]interface{}{},
targetAllocatorEnabled: true,
targetAllocatorRewriteEnabled: false,
expectedError: errors.New("either target allocator or prometheus config needs to be present"),
},
{
description: "target_allocator disabled, config section present",
config: map[interface{}]interface{}{
"config": map[interface{}]interface{}{},
},
targetAllocatorEnabled: false,
targetAllocatorRewriteEnabled: false,
expectedError: nil,
},
{
description: "target_allocator disabled, config section not present",
config: map[interface{}]interface{}{},
targetAllocatorEnabled: false,
targetAllocatorRewriteEnabled: false,
expectedError: fmt.Errorf("no %s available as part of the configuration", "prometheusConfig"),
},
}

for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.description, func(t *testing.T) {
err := ta.ValidatePromConfig(testCase.config, testCase.targetAllocatorEnabled, testCase.targetAllocatorRewriteEnabled)
assert.Equal(t, testCase.expectedError, err)
})
}
}