diff --git a/internal/manifests/collector/targetallocator.go b/internal/manifests/collector/targetallocator.go index cf1e70ff55..c37c220a45 100644 --- a/internal/manifests/collector/targetallocator.go +++ b/internal/manifests/collector/targetallocator.go @@ -15,13 +15,11 @@ package collector import ( - "github.com/mitchellh/mapstructure" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" "github.com/open-telemetry/opentelemetry-operator/internal/manifests" - "github.com/open-telemetry/opentelemetry-operator/internal/manifests/targetallocator/adapters" ) // TargetAllocator builds the TargetAllocator CR for the given instance. @@ -32,19 +30,6 @@ func TargetAllocator(params manifests.Params) (*v1alpha1.TargetAllocator, error) return nil, nil } - configStr, err := params.OtelCol.Spec.Config.Yaml() - if err != nil { - return nil, err - } - scrapeConfigs, err := getScrapeConfigs(configStr) - if err != nil { - return nil, err - } - globalConfig, err := getGlobalConfig(params.OtelCol.Spec.Config) - if err != nil { - return nil, err - } - return &v1alpha1.TargetAllocator{ ObjectMeta: metav1.ObjectMeta{ Name: params.OtelCol.Name, @@ -70,50 +55,8 @@ func TargetAllocator(params manifests.Params) (*v1alpha1.TargetAllocator, error) }, AllocationStrategy: taSpec.AllocationStrategy, FilterStrategy: taSpec.FilterStrategy, - ScrapeConfigs: scrapeConfigs, - GlobalConfig: globalConfig, PrometheusCR: taSpec.PrometheusCR, Observability: taSpec.Observability, }, }, nil } - -func getGlobalConfig(otelConfig v1beta1.Config) (v1beta1.AnyConfig, error) { - // TODO: Eventually we should figure out a way to pull this in to the main specification for the TA - type promReceiverConfig struct { - Prometheus struct { - Config struct { - Global map[string]interface{} `mapstructure:"global"` - } `mapstructure:"config"` - } `mapstructure:"prometheus"` - } - decodedConfig := &promReceiverConfig{} - if err := mapstructure.Decode(otelConfig.Receivers.Object, decodedConfig); err != nil { - return v1beta1.AnyConfig{}, err - } - return v1beta1.AnyConfig{ - Object: decodedConfig.Prometheus.Config.Global, - }, nil -} - -func getScrapeConfigs(otelcolConfig string) ([]v1beta1.AnyConfig, error) { - // Collector supports environment variable substitution, but the TA does not. - // TA Scrape Configs should have a single "$", as it does not support env var substitution - prometheusReceiverConfig, err := adapters.UnescapeDollarSignsInPromConfig(otelcolConfig) - if err != nil { - return nil, err - } - - scrapeConfigs, err := adapters.GetScrapeConfigsFromPromConfig(prometheusReceiverConfig) - if err != nil { - return nil, err - } - - v1beta1scrapeConfigs := make([]v1beta1.AnyConfig, len(scrapeConfigs)) - - for i, config := range scrapeConfigs { - v1beta1scrapeConfigs[i] = v1beta1.AnyConfig{Object: config} - } - - return v1beta1scrapeConfigs, nil -} diff --git a/internal/manifests/collector/targetallocator_test.go b/internal/manifests/collector/targetallocator_test.go index ababa4acb7..3d281d69fd 100644 --- a/internal/manifests/collector/targetallocator_test.go +++ b/internal/manifests/collector/targetallocator_test.go @@ -15,12 +15,10 @@ package collector import ( - "fmt" "testing" "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -89,10 +87,7 @@ func TestTargetAllocator(t *testing.T) { }, want: &v1alpha1.TargetAllocator{ ObjectMeta: objectMetadata, - Spec: v1alpha1.TargetAllocatorSpec{ - ScrapeConfigs: []v1beta1.AnyConfig{}, - GlobalConfig: v1beta1.AnyConfig{}, - }, + Spec: v1alpha1.TargetAllocatorSpec{}, }, }, { @@ -291,7 +286,6 @@ func TestTargetAllocator(t *testing.T) { MatchLabels: map[string]string{"servicemonitorkey": "servicemonitorkey"}, }, }, - ScrapeConfigs: []v1beta1.AnyConfig{}, Observability: v1beta1.ObservabilitySpec{ Metrics: v1beta1.MetricsConfigSpec{ EnableMetrics: true, @@ -314,172 +308,3 @@ func TestTargetAllocator(t *testing.T) { }) } } - -func TestGetScrapeConfigs(t *testing.T) { - testCases := []struct { - name string - input v1beta1.Config - want []v1beta1.AnyConfig - wantErr error - }{ - { - name: "empty scrape configs list", - input: v1beta1.Config{ - Receivers: v1beta1.AnyConfig{ - Object: map[string]interface{}{ - "prometheus": map[string]any{ - "config": map[string]any{ - "scrape_configs": []any{}, - }, - }, - }, - }, - }, - want: []v1beta1.AnyConfig{}, - }, - { - name: "no scrape configs key", - input: v1beta1.Config{ - Receivers: v1beta1.AnyConfig{ - Object: map[string]interface{}{ - "prometheus": map[string]any{ - "config": map[string]any{}, - }, - }, - }, - }, - wantErr: fmt.Errorf("no scrape_configs available as part of the configuration"), - }, - { - name: "one scrape config", - input: v1beta1.Config{ - Receivers: v1beta1.AnyConfig{ - Object: map[string]interface{}{ - "prometheus": map[string]any{ - "config": map[string]any{ - "scrape_configs": []any{ - map[string]any{ - "job": "somejob", - }, - }, - }, - }, - }, - }, - }, - want: []v1beta1.AnyConfig{ - {Object: map[string]interface{}{"job": "somejob"}}, - }, - }, - { - name: "regex substitution", - input: v1beta1.Config{ - Receivers: v1beta1.AnyConfig{ - Object: map[string]interface{}{ - "prometheus": map[string]any{ - "config": map[string]any{ - "scrape_configs": []any{ - map[string]any{ - "job": "somejob", - "metric_relabel_configs": []map[string]any{ - { - "action": "labelmap", - "regex": "label_(.+)", - "replacement": "$$1", - }, - }, - }, - }, - }, - }, - }, - }, - }, - want: []v1beta1.AnyConfig{ - {Object: map[string]interface{}{ - "job": "somejob", - "metric_relabel_configs": []any{ - map[any]any{ - "action": "labelmap", - "regex": "label_(.+)", - "replacement": "$1", - }, - }, - }}, - }, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - configStr, err := testCase.input.Yaml() - require.NoError(t, err) - actual, err := getScrapeConfigs(configStr) - assert.Equal(t, testCase.wantErr, err) - assert.Equal(t, testCase.want, actual) - }) - } -} - -func Test_getGlobalConfig(t *testing.T) { - type args struct { - otelConfig v1beta1.Config - } - tests := []struct { - name string - args args - want v1beta1.AnyConfig - wantErr error - }{ - { - name: "Valid Global Config", - args: args{ - otelConfig: v1beta1.Config{ - Receivers: v1beta1.AnyConfig{ - Object: map[string]interface{}{ - "prometheus": map[string]interface{}{ - "config": map[string]interface{}{ - "global": map[string]interface{}{ - "scrape_interval": "15s", - "scrape_protocols": []string{"PrometheusProto", "OpenMetricsText1.0.0", "OpenMetricsText0.0.1", "PrometheusText0.0.4"}, - }, - }, - }, - }, - }, - }, - }, - want: v1beta1.AnyConfig{ - Object: map[string]interface{}{ - "scrape_interval": "15s", - "scrape_protocols": []string{"PrometheusProto", "OpenMetricsText1.0.0", "OpenMetricsText0.0.1", "PrometheusText0.0.4"}, - }, - }, - wantErr: nil, - }, - { - name: "Invalid Global Config - Missing Global", - args: args{ - otelConfig: v1beta1.Config{ - Receivers: v1beta1.AnyConfig{ - Object: map[string]interface{}{ - "prometheus": map[string]interface{}{ - "config": map[string]interface{}{}, - }, - }, - }, - }, - }, - want: v1beta1.AnyConfig{}, - wantErr: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := getGlobalConfig(tt.args.otelConfig) - assert.Equal(t, tt.wantErr, err) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/internal/manifests/targetallocator/configmap.go b/internal/manifests/targetallocator/configmap.go index eca98615f7..f7e0408974 100644 --- a/internal/manifests/targetallocator/configmap.go +++ b/internal/manifests/targetallocator/configmap.go @@ -15,6 +15,7 @@ package targetallocator import ( + "github.com/mitchellh/mapstructure" "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,6 +23,7 @@ import ( "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils" + "github.com/open-telemetry/opentelemetry-operator/internal/manifests/targetallocator/adapters" "github.com/open-telemetry/opentelemetry-operator/internal/naming" ) @@ -43,13 +45,22 @@ func ConfigMap(params Params) (*corev1.ConfigMap, error) { // Set config if global or scrape configs set config := map[string]interface{}{} - if instance.Spec.GlobalConfig.Object != nil { - config["global"] = instance.Spec.GlobalConfig + globalConfig, err := getGlobalConfig(taSpec.GlobalConfig, params.Collector.Spec.Config) + if err != nil { + return nil, err + } + if len(globalConfig) > 0 { + config["global"] = globalConfig + } + + scrapeConfigs, err := getScrapeConfigs(taSpec.ScrapeConfigs, params.Collector.Spec.Config) + if err != nil { + return nil, err } - // Add scrape configs if present - if instance.Spec.ScrapeConfigs != nil && len(instance.Spec.ScrapeConfigs) > 0 { - config["scrape_configs"] = instance.Spec.ScrapeConfigs + if len(scrapeConfigs) > 0 { + config["scrape_configs"] = scrapeConfigs } + if len(config) != 0 { taConfig["config"] = config } @@ -93,3 +104,77 @@ func ConfigMap(params Params) (*corev1.ConfigMap, error) { }, }, nil } + +func getGlobalConfig(taGlobalConfig v1beta1.AnyConfig, collectorConfig v1beta1.Config) (map[string]any, error) { + // global config from the target allocator has priority + if len(taGlobalConfig.Object) > 0 { + return taGlobalConfig.Object, nil + } + + collectorGlobalConfig, err := getGlobalConfigFromOtelConfig(collectorConfig) + if err != nil { + return nil, err + } + return collectorGlobalConfig.Object, nil +} + +func getScrapeConfigs(taScrapeConfigs []v1beta1.AnyConfig, collectorConfig v1beta1.Config) ([]v1beta1.AnyConfig, error) { + scrapeConfigs := []v1beta1.AnyConfig{} + + // we take scrape configs from both the target allocator spec and the collector config + if len(taScrapeConfigs) > 0 { + scrapeConfigs = append(scrapeConfigs, taScrapeConfigs...) + } + + configStr, err := collectorConfig.Yaml() + if err != nil { + return nil, err + } + + collectorScrapeConfigs, err := getScrapeConfigsFromOtelConfig(configStr) + if err != nil { + return nil, err + } + + return append(scrapeConfigs, collectorScrapeConfigs...), nil +} + +func getGlobalConfigFromOtelConfig(otelConfig v1beta1.Config) (v1beta1.AnyConfig, error) { + // TODO: Eventually we should figure out a way to pull this in to the main specification for the TA + type promReceiverConfig struct { + Prometheus struct { + Config struct { + Global map[string]interface{} `mapstructure:"global"` + } `mapstructure:"config"` + } `mapstructure:"prometheus"` + } + decodedConfig := &promReceiverConfig{} + if err := mapstructure.Decode(otelConfig.Receivers.Object, decodedConfig); err != nil { + return v1beta1.AnyConfig{}, err + } + return v1beta1.AnyConfig{ + Object: decodedConfig.Prometheus.Config.Global, + }, nil +} + +func getScrapeConfigsFromOtelConfig(otelcolConfig string) ([]v1beta1.AnyConfig, error) { + // Collector supports environment variable substitution, but the TA does not. + // TA Scrape Configs should have a single "$", as it does not support env var substitution + prometheusReceiverConfig, err := adapters.UnescapeDollarSignsInPromConfig(otelcolConfig) + if err != nil { + return nil, err + } + + scrapeConfigs, err := adapters.GetScrapeConfigsFromPromConfig(prometheusReceiverConfig) + if err != nil { + return nil, err + } + + v1beta1scrapeConfigs := make([]v1beta1.AnyConfig, len(scrapeConfigs)) + + for i, config := range scrapeConfigs { + v1beta1scrapeConfigs[i] = v1beta1.AnyConfig{Object: config} + } + + return v1beta1scrapeConfigs, nil +} diff --git a/internal/manifests/targetallocator/configmap_test.go b/internal/manifests/targetallocator/configmap_test.go index a8b83dfc4f..cfa45feb8c 100644 --- a/internal/manifests/targetallocator/configmap_test.go +++ b/internal/manifests/targetallocator/configmap_test.go @@ -15,10 +15,12 @@ package targetallocator import ( + "fmt" "testing" "time" "github.com/go-logr/logr" + "github.com/mitchellh/mapstructure" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -96,8 +98,19 @@ filter_strategy: relabel-config targetAllocator = targetAllocatorInstance() targetAllocator.Spec.ScrapeConfigs = []v1beta1.AnyConfig{} params.TargetAllocator = targetAllocator - actual, err := ConfigMap(params) + collectorWithoutPrometheusReceiver := collectorInstance() + collectorWithoutPrometheusReceiver.Spec.Config.Receivers.Object["prometheus"] = map[string]any{ + "config": map[string]any{ + "scrape_configs": []any{}, + }, + } + testParams := Params{ + Collector: collectorWithoutPrometheusReceiver, + TargetAllocator: targetAllocator, + } + actual, err := ConfigMap(testParams) require.NoError(t, err) + params.Collector = collector assert.Equal(t, "my-instance-targetallocator", actual.Name) assert.Equal(t, expectedLabels, actual.Labels) @@ -214,3 +227,365 @@ prometheus_cr: }) } + +func TestGetScrapeConfigsFromOtelConfig(t *testing.T) { + testCases := []struct { + name string + input v1beta1.Config + want []v1beta1.AnyConfig + wantErr error + }{ + { + name: "empty scrape configs list", + input: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]any{ + "config": map[string]any{ + "scrape_configs": []any{}, + }, + }, + }, + }, + }, + want: []v1beta1.AnyConfig{}, + }, + { + name: "no scrape configs key", + input: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]any{ + "config": map[string]any{}, + }, + }, + }, + }, + wantErr: fmt.Errorf("no scrape_configs available as part of the configuration"), + }, + { + name: "one scrape config", + input: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]any{ + "config": map[string]any{ + "scrape_configs": []any{ + map[string]any{ + "job": "somejob", + }, + }, + }, + }, + }, + }, + }, + want: []v1beta1.AnyConfig{ + {Object: map[string]interface{}{"job": "somejob"}}, + }, + }, + { + name: "regex substitution", + input: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]any{ + "config": map[string]any{ + "scrape_configs": []any{ + map[string]any{ + "job": "somejob", + "metric_relabel_configs": []map[string]any{ + { + "action": "labelmap", + "regex": "label_(.+)", + "replacement": "$$1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: []v1beta1.AnyConfig{ + {Object: map[string]interface{}{ + "job": "somejob", + "metric_relabel_configs": []any{ + map[any]any{ + "action": "labelmap", + "regex": "label_(.+)", + "replacement": "$1", + }, + }, + }}, + }, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + configStr, err := testCase.input.Yaml() + require.NoError(t, err) + actual, err := getScrapeConfigsFromOtelConfig(configStr) + assert.Equal(t, testCase.wantErr, err) + assert.Equal(t, testCase.want, actual) + }) + } +} + +func TestGetGlobalConfigFromOtelConfig(t *testing.T) { + type args struct { + otelConfig v1beta1.Config + } + tests := []struct { + name string + args args + want v1beta1.AnyConfig + wantErr error + }{ + { + name: "Valid Global Config", + args: args{ + otelConfig: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]interface{}{ + "config": map[string]interface{}{ + "global": map[string]interface{}{ + "scrape_interval": "15s", + "scrape_protocols": []string{"PrometheusProto", "OpenMetricsText1.0.0", "OpenMetricsText0.0.1", "PrometheusText0.0.4"}, + }, + }, + }, + }, + }, + }, + }, + want: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "scrape_interval": "15s", + "scrape_protocols": []string{"PrometheusProto", "OpenMetricsText1.0.0", "OpenMetricsText0.0.1", "PrometheusText0.0.4"}, + }, + }, + wantErr: nil, + }, + { + name: "Invalid Global Config - Missing Global", + args: args{ + otelConfig: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]interface{}{ + "config": map[string]interface{}{}, + }, + }, + }, + }, + }, + want: v1beta1.AnyConfig{}, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getGlobalConfigFromOtelConfig(tt.args.otelConfig) + assert.Equal(t, tt.wantErr, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestGetScrapeConfigs(t *testing.T) { + type args struct { + taScrapeConfigs []v1beta1.AnyConfig + collectorConfig v1beta1.Config + } + testCases := []struct { + name string + args args + want []v1beta1.AnyConfig + wantErr error + }{ + { + name: "no scrape configs", + args: args{ + taScrapeConfigs: []v1beta1.AnyConfig{}, + collectorConfig: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]any{ + "config": map[string]any{ + "scrape_configs": []any{}, + }, + }, + }, + }, + }, + }, + want: []v1beta1.AnyConfig{}, + }, + { + name: "scrape configs in both ta and collector", + args: args{ + taScrapeConfigs: []v1beta1.AnyConfig{ + { + Object: map[string]any{ + "job": "ta", + }, + }, + }, + collectorConfig: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]any{ + "config": map[string]any{ + "scrape_configs": []any{ + map[string]any{ + "job": "collector", + }, + }, + }, + }, + }, + }, + }, + }, + want: []v1beta1.AnyConfig{ + {Object: map[string]any{"job": "ta"}}, + {Object: map[string]any{"job": "collector"}}, + }, + }, + { + name: "no scrape configs key", + args: args{ + taScrapeConfigs: []v1beta1.AnyConfig{}, + collectorConfig: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]any{ + "config": map[string]any{}, + }, + }, + }, + }, + }, + wantErr: fmt.Errorf("no scrape_configs available as part of the configuration"), + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + actual, err := getScrapeConfigs(testCase.args.taScrapeConfigs, testCase.args.collectorConfig) + assert.Equal(t, testCase.wantErr, err) + assert.Equal(t, testCase.want, actual) + }) + } +} + +func TestGetGlobalConfig(t *testing.T) { + type args struct { + taGlobalConfig v1beta1.AnyConfig + collectorConfig v1beta1.Config + } + tests := []struct { + name string + args args + want map[string]any + wantErr error + }{ + { + name: "Valid Global Config in both TA and Collector, TA wins", + args: args{ + collectorConfig: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]interface{}{ + "config": map[string]interface{}{ + "global": map[string]interface{}{ + "scrape_interval": "15s", + }, + }, + }, + }, + }, + }, + taGlobalConfig: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "scrape_protocols": []string{"PrometheusProto"}, + }, + }, + }, + want: map[string]interface{}{ + "scrape_protocols": []string{"PrometheusProto"}, + }, + }, + { + name: "Valid Global Config in TA, not in Collector", + args: args{ + collectorConfig: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]interface{}{ + "config": map[string]interface{}{}, + }, + }, + }, + }, + taGlobalConfig: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "scrape_protocols": []string{"PrometheusProto"}, + }, + }, + }, + want: map[string]interface{}{ + "scrape_protocols": []string{"PrometheusProto"}, + }, + }, + { + name: "Valid Global Config in Collector, not in TA", + args: args{ + collectorConfig: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": map[string]interface{}{ + "config": map[string]interface{}{ + "global": map[string]interface{}{ + "scrape_interval": "15s", + }, + }, + }, + }, + }, + }, + taGlobalConfig: v1beta1.AnyConfig{}, + }, + want: map[string]interface{}{ + "scrape_interval": "15s", + }, + }, + { + name: "Invalid Global Config in Collector, not in TA", + args: args{ + collectorConfig: v1beta1.Config{ + Receivers: v1beta1.AnyConfig{ + Object: map[string]interface{}{ + "prometheus": "invalid_value", + }, + }, + }, + taGlobalConfig: v1beta1.AnyConfig{}, + }, + wantErr: &mapstructure.Error{Errors: []string{"'prometheus' expected a map, got 'string'"}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getGlobalConfig(tt.args.taGlobalConfig, tt.args.collectorConfig) + assert.Equal(t, tt.wantErr, err) + assert.Equal(t, tt.want, got) + }) + } +}