diff --git a/api/v1alpha1/vector_common_types.go b/api/v1alpha1/vector_common_types.go
index c1cc3c1..712b021 100644
--- a/api/v1alpha1/vector_common_types.go
+++ b/api/v1alpha1/vector_common_types.go
@@ -84,6 +84,16 @@ type VectorCommon struct {
// Enable internal metrics exporter
// +optional
InternalMetrics bool `json:"internalMetrics,omitempty"`
+ // ScrapeInterval defines the interval at which Prometheus should scrape metrics.
+ // Example values: "30s", "1m", "5m". If not specified, Prometheus default is used.
+ // +optional
+ // +kubebuilder:validation:Pattern=`^(0|([0-9]+(\.[0-9]+)?(ms|s|m|h))+)$`
+ ScrapeInterval string `json:"scrapeInterval,omitempty"`
+ // ScrapeTimeout defines the timeout for scraping metrics.
+ // Example values: "10s", "30s". Must be less than ScrapeInterval. If not specified, Prometheus default is used.
+ // +optional
+ // +kubebuilder:validation:Pattern=`^(0|([0-9]+(\.[0-9]+)?(ms|s|m|h))+)$`
+ ScrapeTimeout string `json:"scrapeTimeout,omitempty"`
// List of volumes that can be mounted by containers belonging to the pod.
// +optional
Volumes []v1.Volume `json:"volumes,omitempty"`
diff --git a/config/crd/bases/observability.kaasops.io_clustervectoraggregators.yaml b/config/crd/bases/observability.kaasops.io_clustervectoraggregators.yaml
index d56d862..ab6fa3d 100644
--- a/config/crd/bases/observability.kaasops.io_clustervectoraggregators.yaml
+++ b/config/crd/bases/observability.kaasops.io_clustervectoraggregators.yaml
@@ -3147,6 +3147,18 @@ spec:
schedulerName:
description: SchedulerName - defines kubernetes scheduler name
type: string
+ scrapeInterval:
+ description: |-
+ ScrapeInterval defines the interval at which Prometheus should scrape metrics.
+ Example values: "30s", "1m", "5m". If not specified, Prometheus default is used.
+ pattern: ^(0|([0-9]+(\.[0-9]+)?(ms|s|m|h))+)$
+ type: string
+ scrapeTimeout:
+ description: |-
+ ScrapeTimeout defines the timeout for scraping metrics.
+ Example values: "10s", "30s". Must be less than ScrapeInterval. If not specified, Prometheus default is used.
+ pattern: ^(0|([0-9]+(\.[0-9]+)?(ms|s|m|h))+)$
+ type: string
selector:
description: |-
Selector defines a filter for the Vector Pipeline and Cluster Vector Pipeline by labels.
diff --git a/config/crd/bases/observability.kaasops.io_vectoraggregators.yaml b/config/crd/bases/observability.kaasops.io_vectoraggregators.yaml
index 6497664..d795466 100644
--- a/config/crd/bases/observability.kaasops.io_vectoraggregators.yaml
+++ b/config/crd/bases/observability.kaasops.io_vectoraggregators.yaml
@@ -3141,6 +3141,18 @@ spec:
schedulerName:
description: SchedulerName - defines kubernetes scheduler name
type: string
+ scrapeInterval:
+ description: |-
+ ScrapeInterval defines the interval at which Prometheus should scrape metrics.
+ Example values: "30s", "1m", "5m". If not specified, Prometheus default is used.
+ pattern: ^(0|([0-9]+(\.[0-9]+)?(ms|s|m|h))+)$
+ type: string
+ scrapeTimeout:
+ description: |-
+ ScrapeTimeout defines the timeout for scraping metrics.
+ Example values: "10s", "30s". Must be less than ScrapeInterval. If not specified, Prometheus default is used.
+ pattern: ^(0|([0-9]+(\.[0-9]+)?(ms|s|m|h))+)$
+ type: string
selector:
description: |-
Selector defines a filter for the Vector Pipeline and Cluster Vector Pipeline by labels.
diff --git a/config/crd/bases/observability.kaasops.io_vectors.yaml b/config/crd/bases/observability.kaasops.io_vectors.yaml
index 53577da..ddb3cce 100644
--- a/config/crd/bases/observability.kaasops.io_vectors.yaml
+++ b/config/crd/bases/observability.kaasops.io_vectors.yaml
@@ -3149,6 +3149,18 @@ spec:
schedulerName:
description: SchedulerName - defines kubernetes scheduler name
type: string
+ scrapeInterval:
+ description: |-
+ ScrapeInterval defines the interval at which Prometheus should scrape metrics.
+ Example values: "30s", "1m", "5m". If not specified, Prometheus default is used.
+ pattern: ^(0|([0-9]+(\.[0-9]+)?(ms|s|m|h))+)$
+ type: string
+ scrapeTimeout:
+ description: |-
+ ScrapeTimeout defines the timeout for scraping metrics.
+ Example values: "10s", "30s". Must be less than ScrapeInterval. If not specified, Prometheus default is used.
+ pattern: ^(0|([0-9]+(\.[0-9]+)?(ms|s|m|h))+)$
+ type: string
tolerations:
description: Tolerations If specified, the pod's tolerations.
items:
diff --git a/docs/specification.md b/docs/specification.md
index 5eb3000..96512ca 100644
--- a/docs/specification.md
+++ b/docs/specification.md
@@ -10,7 +10,7 @@
# Vector Spec
- | agent |
+ agent |
image |
Image for Vector agent. timberio/vector:0.48.0-distroless-libc by default |
@@ -26,6 +26,18 @@
api |
ApiSpec |
+
+ | internalMetrics |
+ Enable internal metrics exporter. When enabled, a PodMonitor resource is created for Prometheus scraping. By default - false |
+
+
+ | scrapeInterval |
+ Interval at which Prometheus should scrape metrics from the internal metrics exporter. Examples: "30s", "1m", "5m". Only used when internalMetrics is true. If not specified, Prometheus default is used. |
+
+
+ | scrapeTimeout |
+ Timeout for scraping metrics. Must be less than scrapeInterval. Examples: "10s", "30s". Only used when internalMetrics is true. If not specified, Prometheus default is used. |
+
| service |
Temporary field for enabling service for Vector DaemonSet. By default - false |
diff --git a/internal/vector/aggregator/podmonitor.go b/internal/vector/aggregator/podmonitor.go
index cbf6955..48b2ca8 100644
--- a/internal/vector/aggregator/podmonitor.go
+++ b/internal/vector/aggregator/podmonitor.go
@@ -22,15 +22,22 @@ func (ctrl *Controller) createVectorAggregatorPodMonitor() *monitorv1.PodMonitor
matchLabels := ctrl.matchLabelsForVectorAggregator()
annotations := ctrl.annotationsForVectorAggregator()
+ endpoint := monitorv1.PodMetricsEndpoint{
+ Path: "/metrics",
+ Port: "prom-exporter",
+ }
+
+ if ctrl.Spec.ScrapeInterval != "" {
+ endpoint.Interval = monitorv1.Duration(ctrl.Spec.ScrapeInterval)
+ }
+ if ctrl.Spec.ScrapeTimeout != "" {
+ endpoint.ScrapeTimeout = monitorv1.Duration(ctrl.Spec.ScrapeTimeout)
+ }
+
podmonitor := &monitorv1.PodMonitor{
ObjectMeta: ctrl.objectMetaVectorAggregator(labels, annotations, ctrl.Namespace),
Spec: monitorv1.PodMonitorSpec{
- PodMetricsEndpoints: []monitorv1.PodMetricsEndpoint{
- {
- Path: "/metrics",
- Port: "prom-exporter",
- },
- },
+ PodMetricsEndpoints: []monitorv1.PodMetricsEndpoint{endpoint},
Selector: metav1.LabelSelector{
MatchLabels: matchLabels,
},
diff --git a/internal/vector/aggregator/podmonitor_test.go b/internal/vector/aggregator/podmonitor_test.go
new file mode 100644
index 0000000..b6f44fb
--- /dev/null
+++ b/internal/vector/aggregator/podmonitor_test.go
@@ -0,0 +1,227 @@
+package aggregator
+
+import (
+ "testing"
+
+ . "github.com/onsi/gomega"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ vectorv1alpha1 "github.com/kaasops/vector-operator/api/v1alpha1"
+)
+
+// Helper function to create a Controller for testing
+func createTestController(name, namespace string, spec *vectorv1alpha1.VectorAggregatorCommon, isCluster bool) *Controller {
+ agg := &vectorv1alpha1.VectorAggregator{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Namespace: namespace,
+ },
+ }
+
+ return &Controller{
+ Name: name,
+ Namespace: namespace,
+ VectorAggregator: agg,
+ APIVersion: "observability.kaasops.io/v1alpha1",
+ Kind: "VectorAggregator",
+ Spec: spec,
+ isClusterAggregator: isCluster,
+ }
+}
+
+func TestCreateVectorAggregatorPodMonitor_WithCustomSettings(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := createTestController("test-aggregator", "default",
+ &vectorv1alpha1.VectorAggregatorCommon{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ ScrapeInterval: "60s",
+ ScrapeTimeout: "20s",
+ },
+ }, false)
+
+ pm := ctrl.createVectorAggregatorPodMonitor()
+
+ // Verify PodMonitor structure
+ g.Expect(pm).NotTo(BeNil(), "PodMonitor should not be nil")
+ g.Expect(pm.Spec.PodMetricsEndpoints).To(HaveLen(1), "Should have exactly one endpoint")
+
+ // Verify scrape settings
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+ g.Expect(string(endpoint.Interval)).To(Equal("60s"), "scrapeInterval should be 60s")
+ g.Expect(string(endpoint.ScrapeTimeout)).To(Equal("20s"), "scrapeTimeout should be 20s")
+
+ // Verify endpoint configuration
+ g.Expect(endpoint.Port).To(Equal("prom-exporter"), "Port should be prom-exporter")
+ g.Expect(endpoint.Path).To(Equal("/metrics"), "Path should be /metrics")
+
+ // Verify metadata
+ g.Expect(pm.ObjectMeta.Namespace).To(Equal("default"), "Namespace should match Aggregator namespace")
+}
+
+func TestCreateVectorAggregatorPodMonitor_WithDefaults(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := createTestController("test-aggregator", "default",
+ &vectorv1alpha1.VectorAggregatorCommon{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ // No scrape settings specified
+ },
+ }, false)
+
+ pm := ctrl.createVectorAggregatorPodMonitor()
+
+ g.Expect(pm).NotTo(BeNil())
+ g.Expect(pm.Spec.PodMetricsEndpoints).To(HaveLen(1))
+
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+ // When not specified, fields should be empty (Prometheus will use defaults)
+ g.Expect(string(endpoint.Interval)).To(BeEmpty(), "Interval should be empty when not specified")
+ g.Expect(string(endpoint.ScrapeTimeout)).To(BeEmpty(), "ScrapeTimeout should be empty when not specified")
+
+ // Basic endpoint config should still be set
+ g.Expect(endpoint.Port).To(Equal("prom-exporter"))
+ g.Expect(endpoint.Path).To(Equal("/metrics"))
+}
+
+func TestCreateVectorAggregatorPodMonitor_LabelSelector(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := createTestController("test-aggregator", "test-namespace",
+ &vectorv1alpha1.VectorAggregatorCommon{}, false)
+
+ pm := ctrl.createVectorAggregatorPodMonitor()
+
+ // Verify selector has proper labels to target only Aggregator pods
+ g.Expect(pm.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app.kubernetes.io/component", "Aggregator"),
+ "Selector should include component=Aggregator label")
+ g.Expect(pm.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app.kubernetes.io/instance", "test-aggregator"),
+ "Selector should include instance label matching Aggregator name")
+}
+
+func TestCreateVectorAggregatorPodMonitor_OnlyIntervalSet(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := createTestController("test-aggregator", "default",
+ &vectorv1alpha1.VectorAggregatorCommon{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ ScrapeInterval: "1m",
+ // ScrapeTimeout not set
+ },
+ }, false)
+
+ pm := ctrl.createVectorAggregatorPodMonitor()
+
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+ g.Expect(string(endpoint.Interval)).To(Equal("1m"), "Interval should be set")
+ g.Expect(string(endpoint.ScrapeTimeout)).To(BeEmpty(), "Timeout should remain empty")
+}
+
+func TestCreateVectorAggregatorPodMonitor_OnlyTimeoutSet(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := createTestController("test-aggregator", "default",
+ &vectorv1alpha1.VectorAggregatorCommon{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ // ScrapeInterval not set
+ ScrapeTimeout: "30s",
+ },
+ }, false)
+
+ pm := ctrl.createVectorAggregatorPodMonitor()
+
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+ g.Expect(string(endpoint.Interval)).To(BeEmpty(), "Interval should remain empty")
+ g.Expect(string(endpoint.ScrapeTimeout)).To(Equal("30s"), "Timeout should be set")
+}
+
+func TestCreateVectorAggregatorPodMonitor_DurationFormats(t *testing.T) {
+ testCases := []struct {
+ name string
+ interval string
+ timeout string
+ expectedInt string
+ expectedTime string
+ }{
+ {
+ name: "Seconds format",
+ interval: "60s",
+ timeout: "20s",
+ expectedInt: "60s",
+ expectedTime: "20s",
+ },
+ {
+ name: "Minutes format",
+ interval: "10m",
+ timeout: "2m",
+ expectedInt: "10m",
+ expectedTime: "2m",
+ },
+ {
+ name: "Mixed format",
+ interval: "2m30s",
+ timeout: "45s",
+ expectedInt: "2m30s",
+ expectedTime: "45s",
+ },
+ {
+ name: "Milliseconds format",
+ interval: "1000ms",
+ timeout: "500ms",
+ expectedInt: "1000ms",
+ expectedTime: "500ms",
+ },
+ {
+ name: "Hours format",
+ interval: "2h",
+ timeout: "1h",
+ expectedInt: "2h",
+ expectedTime: "1h",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := createTestController("test-aggregator", "default",
+ &vectorv1alpha1.VectorAggregatorCommon{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ ScrapeInterval: tc.interval,
+ ScrapeTimeout: tc.timeout,
+ },
+ }, false)
+
+ pm := ctrl.createVectorAggregatorPodMonitor()
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+
+ g.Expect(string(endpoint.Interval)).To(Equal(tc.expectedInt))
+ g.Expect(string(endpoint.ScrapeTimeout)).To(Equal(tc.expectedTime))
+ })
+ }
+}
+
+func TestCreateVectorAggregatorPodMonitor_ClusterAggregator(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := createTestController("cluster-test-aggregator", "vector-system",
+ &vectorv1alpha1.VectorAggregatorCommon{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ ScrapeInterval: "90s",
+ ScrapeTimeout: "25s",
+ },
+ }, true)
+
+ pm := ctrl.createVectorAggregatorPodMonitor()
+
+ // Verify ClusterVectorAggregator also gets proper PodMonitor
+ g.Expect(pm).NotTo(BeNil())
+
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+ g.Expect(string(endpoint.Interval)).To(Equal("90s"))
+ g.Expect(string(endpoint.ScrapeTimeout)).To(Equal("25s"))
+
+ // Verify selector for ClusterVectorAggregator
+ g.Expect(pm.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app.kubernetes.io/component", "Aggregator"))
+ g.Expect(pm.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app.kubernetes.io/instance", "cluster-test-aggregator"))
+}
diff --git a/internal/vector/vectoragent/vectoragent_podmonitor.go b/internal/vector/vectoragent/vectoragent_podmonitor.go
index dd09077..ae44d7e 100644
--- a/internal/vector/vectoragent/vectoragent_podmonitor.go
+++ b/internal/vector/vectoragent/vectoragent_podmonitor.go
@@ -11,15 +11,22 @@ func (ctrl *Controller) createVectorAgentPodMonitor() *monitorv1.PodMonitor {
matchLabels := ctrl.matchLabelsForVectorAgent()
annotations := ctrl.annotationsForVectorAgent()
+ endpoint := monitorv1.PodMetricsEndpoint{
+ Path: "/metrics",
+ Port: "prom-exporter",
+ }
+
+ if ctrl.Vector.Spec.Agent.ScrapeInterval != "" {
+ endpoint.Interval = monitorv1.Duration(ctrl.Vector.Spec.Agent.ScrapeInterval)
+ }
+ if ctrl.Vector.Spec.Agent.ScrapeTimeout != "" {
+ endpoint.ScrapeTimeout = monitorv1.Duration(ctrl.Vector.Spec.Agent.ScrapeTimeout)
+ }
+
podmonitor := &monitorv1.PodMonitor{
ObjectMeta: ctrl.objectMetaVectorAgent(labels, annotations, ctrl.Vector.Namespace),
Spec: monitorv1.PodMonitorSpec{
- PodMetricsEndpoints: []monitorv1.PodMetricsEndpoint{
- {
- Path: "/metrics",
- Port: "prom-exporter",
- },
- },
+ PodMetricsEndpoints: []monitorv1.PodMetricsEndpoint{endpoint},
Selector: metav1.LabelSelector{
MatchLabels: matchLabels,
},
diff --git a/internal/vector/vectoragent/vectoragent_podmonitor_test.go b/internal/vector/vectoragent/vectoragent_podmonitor_test.go
new file mode 100644
index 0000000..741d38d
--- /dev/null
+++ b/internal/vector/vectoragent/vectoragent_podmonitor_test.go
@@ -0,0 +1,236 @@
+package vectoragent
+
+import (
+ "testing"
+
+ . "github.com/onsi/gomega"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ vectorv1alpha1 "github.com/kaasops/vector-operator/api/v1alpha1"
+)
+
+func TestCreateVectorAgentPodMonitor_WithCustomSettings(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := &Controller{
+ Vector: &vectorv1alpha1.Vector{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-vector",
+ Namespace: "default",
+ },
+ Spec: vectorv1alpha1.VectorSpec{
+ Agent: &vectorv1alpha1.VectorAgent{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ ScrapeInterval: "45s",
+ ScrapeTimeout: "15s",
+ },
+ },
+ },
+ },
+ }
+
+ pm := ctrl.createVectorAgentPodMonitor()
+
+ // Verify PodMonitor structure
+ g.Expect(pm).NotTo(BeNil(), "PodMonitor should not be nil")
+ g.Expect(pm.Spec.PodMetricsEndpoints).To(HaveLen(1), "Should have exactly one endpoint")
+
+ // Verify scrape settings
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+ g.Expect(string(endpoint.Interval)).To(Equal("45s"), "scrapeInterval should be 45s")
+ g.Expect(string(endpoint.ScrapeTimeout)).To(Equal("15s"), "scrapeTimeout should be 15s")
+
+ // Verify endpoint configuration
+ g.Expect(endpoint.Port).To(Equal("prom-exporter"), "Port should be prom-exporter")
+ g.Expect(endpoint.Path).To(Equal("/metrics"), "Path should be /metrics")
+
+ // Verify metadata
+ g.Expect(pm.ObjectMeta.Namespace).To(Equal("default"), "Namespace should match Vector namespace")
+}
+
+func TestCreateVectorAgentPodMonitor_WithDefaults(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := &Controller{
+ Vector: &vectorv1alpha1.Vector{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-vector",
+ Namespace: "default",
+ },
+ Spec: vectorv1alpha1.VectorSpec{
+ Agent: &vectorv1alpha1.VectorAgent{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ // No scrape settings specified
+ },
+ },
+ },
+ },
+ }
+
+ pm := ctrl.createVectorAgentPodMonitor()
+
+ g.Expect(pm).NotTo(BeNil())
+ g.Expect(pm.Spec.PodMetricsEndpoints).To(HaveLen(1))
+
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+ // When not specified, fields should be empty (Prometheus will use defaults)
+ g.Expect(string(endpoint.Interval)).To(BeEmpty(), "Interval should be empty when not specified")
+ g.Expect(string(endpoint.ScrapeTimeout)).To(BeEmpty(), "ScrapeTimeout should be empty when not specified")
+
+ // Basic endpoint config should still be set
+ g.Expect(endpoint.Port).To(Equal("prom-exporter"))
+ g.Expect(endpoint.Path).To(Equal("/metrics"))
+}
+
+func TestCreateVectorAgentPodMonitor_LabelSelector(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := &Controller{
+ Vector: &vectorv1alpha1.Vector{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-vector-agent",
+ Namespace: "test-namespace",
+ },
+ Spec: vectorv1alpha1.VectorSpec{
+ Agent: &vectorv1alpha1.VectorAgent{},
+ },
+ },
+ }
+
+ pm := ctrl.createVectorAgentPodMonitor()
+
+ // Verify selector has proper labels to target only Agent pods
+ g.Expect(pm.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app.kubernetes.io/component", "Agent"),
+ "Selector should include component=Agent label")
+ g.Expect(pm.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app.kubernetes.io/instance", "test-vector-agent"),
+ "Selector should include instance label matching Vector name")
+}
+
+func TestCreateVectorAgentPodMonitor_OnlyIntervalSet(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := &Controller{
+ Vector: &vectorv1alpha1.Vector{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-vector",
+ Namespace: "default",
+ },
+ Spec: vectorv1alpha1.VectorSpec{
+ Agent: &vectorv1alpha1.VectorAgent{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ ScrapeInterval: "30s",
+ // ScrapeTimeout not set
+ },
+ },
+ },
+ },
+ }
+
+ pm := ctrl.createVectorAgentPodMonitor()
+
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+ g.Expect(string(endpoint.Interval)).To(Equal("30s"), "Interval should be set")
+ g.Expect(string(endpoint.ScrapeTimeout)).To(BeEmpty(), "Timeout should remain empty")
+}
+
+func TestCreateVectorAgentPodMonitor_OnlyTimeoutSet(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := &Controller{
+ Vector: &vectorv1alpha1.Vector{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-vector",
+ Namespace: "default",
+ },
+ Spec: vectorv1alpha1.VectorSpec{
+ Agent: &vectorv1alpha1.VectorAgent{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ // ScrapeInterval not set
+ ScrapeTimeout: "10s",
+ },
+ },
+ },
+ },
+ }
+
+ pm := ctrl.createVectorAgentPodMonitor()
+
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+ g.Expect(string(endpoint.Interval)).To(BeEmpty(), "Interval should remain empty")
+ g.Expect(string(endpoint.ScrapeTimeout)).To(Equal("10s"), "Timeout should be set")
+}
+
+func TestCreateVectorAgentPodMonitor_DurationFormats(t *testing.T) {
+ testCases := []struct {
+ name string
+ interval string
+ timeout string
+ expectedInt string
+ expectedTime string
+ }{
+ {
+ name: "Seconds format",
+ interval: "30s",
+ timeout: "10s",
+ expectedInt: "30s",
+ expectedTime: "10s",
+ },
+ {
+ name: "Minutes format",
+ interval: "5m",
+ timeout: "1m",
+ expectedInt: "5m",
+ expectedTime: "1m",
+ },
+ {
+ name: "Mixed format",
+ interval: "1m30s",
+ timeout: "30s",
+ expectedInt: "1m30s",
+ expectedTime: "30s",
+ },
+ {
+ name: "Milliseconds format",
+ interval: "500ms",
+ timeout: "100ms",
+ expectedInt: "500ms",
+ expectedTime: "100ms",
+ },
+ {
+ name: "Hours format",
+ interval: "1h",
+ timeout: "30m",
+ expectedInt: "1h",
+ expectedTime: "30m",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ g := NewWithT(t)
+
+ ctrl := &Controller{
+ Vector: &vectorv1alpha1.Vector{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-vector",
+ Namespace: "default",
+ },
+ Spec: vectorv1alpha1.VectorSpec{
+ Agent: &vectorv1alpha1.VectorAgent{
+ VectorCommon: vectorv1alpha1.VectorCommon{
+ ScrapeInterval: tc.interval,
+ ScrapeTimeout: tc.timeout,
+ },
+ },
+ },
+ },
+ }
+
+ pm := ctrl.createVectorAgentPodMonitor()
+ endpoint := pm.Spec.PodMetricsEndpoints[0]
+
+ g.Expect(string(endpoint.Interval)).To(Equal(tc.expectedInt))
+ g.Expect(string(endpoint.ScrapeTimeout)).To(Equal(tc.expectedTime))
+ })
+ }
+}
diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go
index 32ba098..1b46161 100644
--- a/test/e2e/framework/framework.go
+++ b/test/e2e/framework/framework.go
@@ -280,19 +280,40 @@ func (f *Framework) ApplyTestDataWithoutNamespaceReplacement(path string) {
Expect(err).NotTo(HaveOccurred(), "Failed to apply test data %s", path)
}
-// replaceNamespace replaces hardcoded namespaces in YAML content
+// DeleteTestData loads and deletes a test manifest from testdata directory
+// It automatically replaces any hardcoded namespace with the framework's namespace
+func (f *Framework) DeleteTestData(path string) {
+ By(fmt.Sprintf("deleting test data: %s", path))
+
+ content, err := os.ReadFile(filepath.Join(f.TestDataPath, path))
+ Expect(err).NotTo(HaveOccurred(), "Failed to load test data from %s", path)
+
+ // Replace namespace in YAML if present
+ yamlContent := replaceNamespace(string(content), f.namespace)
+
+ err = f.kubectl.DeleteFromYAML(yamlContent)
+ Expect(err).NotTo(HaveOccurred(), "Failed to delete test data %s in namespace %s", path, f.namespace)
+}
+
+// replaceNamespace replaces namespace placeholders and fields in YAML content
func replaceNamespace(yaml, namespace string) string {
- // This is a simple replacement - for production use, proper YAML parsing might be better
- // But for tests this is sufficient
+ // Replace NAMESPACE placeholders throughout the content
+ // This handles cases like spec.resourceNamespace: NAMESPACE
+ yaml = replacePlaceholder(yaml, "NAMESPACE", namespace)
+
+ // Also replace explicit namespace fields in metadata sections
+ // This handles cases like:
+ // namespace: some-other-namespace
lines := []string{}
for _, line := range splitLines(yaml) {
- // Replace namespace: with namespace:
+ // Check for " namespace:" (2 spaces + "namespace:" = 12 chars)
if len(line) > 12 && line[:12] == " namespace:" {
lines = append(lines, fmt.Sprintf(" namespace: %s", namespace))
} else {
lines = append(lines, line)
}
}
+
return joinLines(lines)
}
diff --git a/test/e2e/framework/kubectl/client.go b/test/e2e/framework/kubectl/client.go
index 6d13ca7..95e2425 100644
--- a/test/e2e/framework/kubectl/client.go
+++ b/test/e2e/framework/kubectl/client.go
@@ -80,6 +80,28 @@ func (c *Client) ApplyWithoutNamespaceOverride(yamlContent string) error {
return err
}
+// DeleteFromYAML deletes resources from YAML content
+func (c *Client) DeleteFromYAML(yamlContent string) error {
+ // Validate namespace to prevent command injection
+ if err := ValidateNamespace(c.namespace); err != nil {
+ return fmt.Errorf("namespace validation failed: %w", err)
+ }
+
+ // Log command for audit and reproducibility
+ log.Printf("KUBECTL_CMD: kubectl delete -f - -n %s", c.namespace)
+
+ cmd := exec.Command("kubectl", "delete", "-f", "-", "-n", c.namespace)
+ cmd.Stdin = strings.NewReader(yamlContent)
+ output, err := utils.Run(cmd)
+
+ // Log kubectl output for debugging
+ if len(output) > 0 {
+ fmt.Printf("kubectl delete: %s\n", string(output))
+ }
+
+ return err
+}
+
// Get retrieves a resource by name and type
func (c *Client) Get(resourceType, name string) ([]byte, error) {
// Validate parameters to prevent command injection
diff --git a/test/e2e/podmonitor_e2e_test.go b/test/e2e/podmonitor_e2e_test.go
new file mode 100644
index 0000000..839acbf
--- /dev/null
+++ b/test/e2e/podmonitor_e2e_test.go
@@ -0,0 +1,460 @@
+/*
+Copyright 2024.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "strings"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/kaasops/vector-operator/test/e2e/framework"
+ "github.com/kaasops/vector-operator/test/e2e/framework/config"
+)
+
+// PodMonitor tests verify the PodMonitor creation and configuration
+// including scrapeInterval and scrapeTimeout settings
+var _ = Describe("PodMonitor Configuration", Label(config.LabelSmoke, config.LabelFast), Ordered, func() {
+ f := framework.NewUniqueFramework("test-podmonitor")
+
+ BeforeAll(func() {
+ f.Setup()
+ })
+
+ AfterAll(func() {
+ f.Teardown()
+ f.PrintMetrics()
+ })
+
+ Context("Agent PodMonitor with custom scrape settings", func() {
+ It("should create PodMonitor with scrapeInterval and scrapeTimeout", func() {
+ By("deploying Vector Agent with custom scrape settings")
+ f.ApplyTestData("podmonitor/agent-with-scrape-config.yaml")
+
+ // Wait for agent resources to be created
+ time.Sleep(5 * time.Second)
+
+ By("verifying PodMonitor is created")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-agent-agent")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed())
+
+ By("verifying scrapeInterval is set correctly")
+ interval, err := getPodMonitorScrapeInterval(f.Namespace(), "podmonitor-agent-agent")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(interval).To(Equal("45s"), "scrapeInterval should be 45s")
+
+ By("verifying scrapeTimeout is set correctly")
+ timeout, err := getPodMonitorScrapeTimeout(f.Namespace(), "podmonitor-agent-agent")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(timeout).To(Equal("15s"), "scrapeTimeout should be 15s")
+ })
+
+ It("should create PodMonitor with default settings when not specified", func() {
+ By("deploying Vector Agent with default settings")
+ f.ApplyTestData("podmonitor/agent-with-defaults.yaml")
+
+ // Wait for agent resources to be created
+ time.Sleep(5 * time.Second)
+
+ By("verifying PodMonitor is created")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-agent-defaults-agent")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed())
+
+ By("verifying scrapeInterval is not set (uses Prometheus default)")
+ interval, err := getPodMonitorScrapeInterval(f.Namespace(), "podmonitor-agent-defaults-agent")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(interval).To(BeEmpty(), "scrapeInterval should be empty when not specified")
+
+ By("verifying scrapeTimeout is not set (uses Prometheus default)")
+ timeout, err := getPodMonitorScrapeTimeout(f.Namespace(), "podmonitor-agent-defaults-agent")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(timeout).To(BeEmpty(), "scrapeTimeout should be empty when not specified")
+ })
+
+ It("should NOT create PodMonitor when internalMetrics is disabled", func() {
+ By("deploying Vector Agent with internalMetrics disabled")
+ f.ApplyTestData("podmonitor/agent-no-metrics.yaml")
+
+ // Wait for agent resources to be created
+ time.Sleep(5 * time.Second)
+
+ By("verifying PodMonitor is NOT created")
+ Consistently(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-agent-no-metrics-agent")
+ }, 10*time.Second, time.Second).Should(HaveOccurred(), "PodMonitor should not exist when internalMetrics is disabled")
+ })
+ })
+
+ Context("Aggregator PodMonitor with custom scrape settings", func() {
+ It("should create PodMonitor with scrapeInterval and scrapeTimeout", func() {
+ By("deploying VectorAggregator with custom scrape settings")
+ f.ApplyTestData("podmonitor/aggregator-with-scrape-config.yaml")
+ f.WaitForDeploymentReady("podmonitor-aggregator-aggregator")
+
+ By("verifying PodMonitor is created")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-aggregator-aggregator")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed())
+
+ By("verifying scrapeInterval is set correctly")
+ interval, err := getPodMonitorScrapeInterval(f.Namespace(), "podmonitor-aggregator-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(interval).To(Equal("60s"), "scrapeInterval should be 60s")
+
+ By("verifying scrapeTimeout is set correctly")
+ timeout, err := getPodMonitorScrapeTimeout(f.Namespace(), "podmonitor-aggregator-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(timeout).To(Equal("20s"), "scrapeTimeout should be 20s")
+ })
+
+ It("should create PodMonitor with default settings when not specified", func() {
+ By("deploying VectorAggregator with default settings")
+ f.ApplyTestData("podmonitor/aggregator-with-defaults.yaml")
+ f.WaitForDeploymentReady("podmonitor-aggregator-defaults-aggregator")
+
+ By("verifying PodMonitor is created")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-aggregator-defaults-aggregator")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed())
+
+ By("verifying scrapeInterval is not set (uses Prometheus default)")
+ interval, err := getPodMonitorScrapeInterval(f.Namespace(), "podmonitor-aggregator-defaults-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(interval).To(BeEmpty(), "scrapeInterval should be empty when not specified")
+
+ By("verifying scrapeTimeout is not set (uses Prometheus default)")
+ timeout, err := getPodMonitorScrapeTimeout(f.Namespace(), "podmonitor-aggregator-defaults-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(timeout).To(BeEmpty(), "scrapeTimeout should be empty when not specified")
+ })
+ })
+
+ Context("PodMonitor label selectors", func() {
+ It("should have correct matchLabels to select only related pods", func() {
+ By("verifying Agent PodMonitor selector")
+ selector, err := getPodMonitorSelector(f.Namespace(), "podmonitor-agent-agent")
+ Expect(err).NotTo(HaveOccurred())
+
+ By("checking selector contains component=Agent")
+ Expect(selector).To(HaveKeyWithValue("app.kubernetes.io/component", "Agent"))
+ Expect(selector).To(HaveKeyWithValue("app.kubernetes.io/instance", "podmonitor-agent"))
+
+ By("verifying Aggregator PodMonitor selector")
+ selectorAgg, err := getPodMonitorSelector(f.Namespace(), "podmonitor-aggregator-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+
+ By("checking selector contains component=Aggregator")
+ Expect(selectorAgg).To(HaveKeyWithValue("app.kubernetes.io/component", "Aggregator"))
+ Expect(selectorAgg).To(HaveKeyWithValue("app.kubernetes.io/instance", "podmonitor-aggregator"))
+ })
+ })
+
+ Context("ClusterVectorAggregator PodMonitor with custom scrape settings", func() {
+ It("should create PodMonitor with scrapeInterval and scrapeTimeout", func() {
+ By("deploying ClusterVectorAggregator with custom scrape settings")
+ f.ApplyTestData("podmonitor/cluster-aggregator-with-scrape-config.yaml")
+ f.WaitForDeploymentReady("podmonitor-cluster-agg-aggregator")
+
+ By("verifying PodMonitor is created")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-cluster-agg-aggregator")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed())
+
+ By("verifying scrapeInterval is set correctly")
+ interval, err := getPodMonitorScrapeInterval(f.Namespace(), "podmonitor-cluster-agg-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(interval).To(Equal("90s"), "scrapeInterval should be 90s")
+
+ By("verifying scrapeTimeout is set correctly")
+ timeout, err := getPodMonitorScrapeTimeout(f.Namespace(), "podmonitor-cluster-agg-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(timeout).To(Equal("25s"), "scrapeTimeout should be 25s")
+ })
+
+ It("should create PodMonitor with default settings when not specified", func() {
+ By("deploying ClusterVectorAggregator with default settings")
+ f.ApplyTestData("podmonitor/cluster-aggregator-with-defaults.yaml")
+ f.WaitForDeploymentReady("podmonitor-cluster-agg-defaults-aggregator")
+
+ By("verifying PodMonitor is created")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-cluster-agg-defaults-aggregator")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed())
+
+ By("verifying scrapeInterval is not set (uses Prometheus default)")
+ interval, err := getPodMonitorScrapeInterval(f.Namespace(), "podmonitor-cluster-agg-defaults-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(interval).To(BeEmpty(), "scrapeInterval should be empty when not specified")
+
+ By("verifying scrapeTimeout is not set (uses Prometheus default)")
+ timeout, err := getPodMonitorScrapeTimeout(f.Namespace(), "podmonitor-cluster-agg-defaults-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(timeout).To(BeEmpty(), "scrapeTimeout should be empty when not specified")
+ })
+
+ It("should have correct matchLabels to select only ClusterVectorAggregator pods", func() {
+ By("verifying ClusterVectorAggregator PodMonitor selector")
+ selector, err := getPodMonitorSelector(f.Namespace(), "podmonitor-cluster-agg-aggregator")
+ Expect(err).NotTo(HaveOccurred())
+
+ By("checking selector contains component=Aggregator")
+ Expect(selector).To(HaveKeyWithValue("app.kubernetes.io/component", "Aggregator"))
+ Expect(selector).To(HaveKeyWithValue("app.kubernetes.io/instance", "podmonitor-cluster-agg"))
+ })
+ })
+})
+
+// InternalMetrics tests verify the isExporterSinkExists logic
+var _ = Describe("Internal Metrics Exporter Logic", Label(config.LabelSmoke, config.LabelFast), Ordered, func() {
+ f := framework.NewUniqueFramework("test-exporter-logic")
+
+ BeforeAll(func() {
+ f.Setup()
+ })
+
+ AfterAll(func() {
+ f.Teardown()
+ f.PrintMetrics()
+ })
+
+ Context("Auto-add prometheus_exporter when not present", func() {
+ It("should add default prometheus_exporter when pipeline has no exporter sink", func() {
+ By("deploying test pod with app=test label")
+ f.ApplyTestData("podmonitor/test-pod.yaml")
+ f.WaitForPodReady("test-app")
+
+ By("deploying Vector Agent with internalMetrics enabled")
+ f.ApplyTestData("podmonitor/agent-with-defaults.yaml")
+ time.Sleep(5 * time.Second)
+
+ By("creating pipeline without prometheus_exporter sink")
+ f.ApplyTestData("podmonitor/pipeline-without-exporter.yaml")
+ f.WaitForPipelineValid("no-exporter-pipeline")
+
+ By("verifying agent config contains default prometheus_exporter")
+ Eventually(func() bool {
+ return checkConfigHasExporter(f.Namespace(), "podmonitor-agent-defaults-agent", "internalMetricsSink")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(BeTrue(),
+ "Agent config should have auto-added prometheus_exporter")
+ })
+ })
+
+ Context("Skip adding prometheus_exporter when already present", func() {
+ It("should NOT add default prometheus_exporter when pipeline already has one", func() {
+ By("deploying Vector Agent with internalMetrics enabled")
+ // Using existing agent from previous test
+
+ By("creating pipeline WITH custom prometheus_exporter sink")
+ f.ApplyTestData("podmonitor/pipeline-with-custom-exporter.yaml")
+ f.WaitForPipelineValid("custom-exporter-pipeline")
+
+ By("verifying agent config uses custom exporter from pipeline")
+ Eventually(func() bool {
+ return checkConfigHasExporter(f.Namespace(), "podmonitor-agent-defaults-agent", "custom_prom_exporter")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(BeTrue(),
+ "Agent config should have custom prometheus_exporter from pipeline")
+
+ By("verifying default exporter is NOT added when custom exporter exists")
+ // When user provides custom prometheus_exporter, the default should NOT be added
+ // because isExporterSinkExists() detects the custom exporter
+ Consistently(func() bool {
+ return !checkConfigHasExporter(f.Namespace(), "podmonitor-agent-defaults-agent", "internalMetricsSink")
+ }, 10*time.Second, 2*time.Second).Should(BeTrue(),
+ "Default exporter should NOT be added when custom exporter exists")
+ })
+ })
+})
+
+// PodMonitor Update tests verify that PodMonitor updates when CRD changes
+var _ = Describe("PodMonitor Update Behavior", Label(config.LabelSmoke, config.LabelFast), Ordered, func() {
+ f := framework.NewUniqueFramework("test-podmonitor-update")
+
+ BeforeAll(func() {
+ f.Setup()
+ })
+
+ AfterAll(func() {
+ f.Teardown()
+ f.PrintMetrics()
+ })
+
+ Context("Update scrapeInterval and scrapeTimeout", func() {
+ It("should update PodMonitor when Agent scrapeInterval changes", func() {
+ By("deploying Vector Agent with initial scrapeInterval=45s")
+ f.ApplyTestData("podmonitor/agent-with-scrape-config.yaml")
+ time.Sleep(5 * time.Second)
+
+ By("verifying initial scrapeInterval is 45s")
+ Eventually(func() string {
+ interval, _ := getPodMonitorScrapeInterval(f.Namespace(), "podmonitor-agent-agent")
+ return interval
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Equal("45s"),
+ "Initial scrapeInterval should be 45s")
+
+ By("updating Vector Agent with new scrapeInterval=90s")
+ f.ApplyTestData("podmonitor/agent-with-updated-interval.yaml")
+
+ By("verifying PodMonitor scrapeInterval updates to 90s")
+ Eventually(func() string {
+ interval, _ := getPodMonitorScrapeInterval(f.Namespace(), "podmonitor-agent-agent")
+ return interval
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Equal("90s"),
+ "Updated scrapeInterval should be 90s")
+
+ By("verifying scrapeTimeout also updates to 30s")
+ Eventually(func() string {
+ timeout, _ := getPodMonitorScrapeTimeout(f.Namespace(), "podmonitor-agent-agent")
+ return timeout
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Equal("30s"),
+ "Updated scrapeTimeout should be 30s")
+ })
+ })
+})
+
+// PodMonitor Cleanup tests verify that PodMonitor is deleted when Vector CR is deleted
+var _ = Describe("PodMonitor Cleanup Behavior", Label(config.LabelSmoke, config.LabelFast), Ordered, func() {
+ f := framework.NewUniqueFramework("test-podmonitor-cleanup")
+
+ BeforeAll(func() {
+ f.Setup()
+ })
+
+ AfterAll(func() {
+ f.Teardown()
+ f.PrintMetrics()
+ })
+
+ Context("Delete Vector CR", func() {
+ It("should delete PodMonitor when Agent is deleted", func() {
+ By("deploying Vector Agent with PodMonitor")
+ f.ApplyTestData("podmonitor/agent-with-scrape-config.yaml")
+ time.Sleep(5 * time.Second)
+
+ By("verifying PodMonitor exists")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-agent-agent")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed(),
+ "PodMonitor should exist after Agent creation")
+
+ By("deleting Vector Agent CR")
+ f.DeleteTestData("podmonitor/agent-with-scrape-config.yaml")
+
+ By("verifying PodMonitor is cleaned up")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-agent-agent")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(HaveOccurred(),
+ "PodMonitor should be deleted when Agent is deleted")
+ })
+
+ It("should delete PodMonitor when Aggregator is deleted", func() {
+ By("deploying VectorAggregator with PodMonitor")
+ f.ApplyTestData("podmonitor/aggregator-with-scrape-config.yaml")
+ f.WaitForDeploymentReady("podmonitor-aggregator-aggregator")
+
+ By("verifying PodMonitor exists")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-aggregator-aggregator")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed(),
+ "PodMonitor should exist after Aggregator creation")
+
+ By("deleting VectorAggregator CR")
+ f.DeleteTestData("podmonitor/aggregator-with-scrape-config.yaml")
+
+ By("verifying PodMonitor is cleaned up")
+ Eventually(func() error {
+ return checkPodMonitorExists(f.Namespace(), "podmonitor-aggregator-aggregator")
+ }, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(HaveOccurred(),
+ "PodMonitor should be deleted when Aggregator is deleted")
+ })
+ })
+})
+
+// Helper functions for PodMonitor verification
+
+func checkPodMonitorExists(namespace, name string) error {
+ cmd := exec.Command("kubectl", "get", "podmonitor", name, "-n", namespace)
+ _, err := cmd.Output()
+ return err
+}
+
+func getPodMonitorScrapeInterval(namespace, name string) (string, error) {
+ cmd := exec.Command("kubectl", "get", "podmonitor", name, "-n", namespace,
+ "-o", "jsonpath={.spec.podMetricsEndpoints[0].interval}")
+ output, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimSpace(string(output)), nil
+}
+
+func getPodMonitorScrapeTimeout(namespace, name string) (string, error) {
+ cmd := exec.Command("kubectl", "get", "podmonitor", name, "-n", namespace,
+ "-o", "jsonpath={.spec.podMetricsEndpoints[0].scrapeTimeout}")
+ output, err := cmd.Output()
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimSpace(string(output)), nil
+}
+
+func getPodMonitorSelector(namespace, name string) (map[string]string, error) {
+ cmd := exec.Command("kubectl", "get", "podmonitor", name, "-n", namespace, "-o", "json")
+ output, err := cmd.Output()
+ if err != nil {
+ return nil, err
+ }
+
+ var podMonitor struct {
+ Spec struct {
+ Selector struct {
+ MatchLabels map[string]string `json:"matchLabels"`
+ } `json:"selector"`
+ } `json:"spec"`
+ }
+
+ if err := json.Unmarshal(output, &podMonitor); err != nil {
+ return nil, fmt.Errorf("failed to parse PodMonitor JSON: %w", err)
+ }
+
+ return podMonitor.Spec.Selector.MatchLabels, nil
+}
+
+func checkConfigHasExporter(namespace, secretName, exporterName string) bool {
+ // Get the secret containing vector config
+ cmd := exec.Command("kubectl", "get", "secret", secretName, "-n", namespace,
+ "-o", "jsonpath={.data['agent\\.json']}")
+ output, err := cmd.Output()
+ if err != nil {
+ return false
+ }
+
+ // Decode base64
+ decoded, err := base64.StdEncoding.DecodeString(string(output))
+ if err != nil {
+ return false
+ }
+
+ // Check if exporter name is in the config
+ return strings.Contains(string(decoded), exporterName)
+}
diff --git a/test/e2e/testdata/podmonitor/agent-no-metrics.yaml b/test/e2e/testdata/podmonitor/agent-no-metrics.yaml
new file mode 100644
index 0000000..ab7820e
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/agent-no-metrics.yaml
@@ -0,0 +1,8 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: Vector
+metadata:
+ name: podmonitor-agent-no-metrics
+spec:
+ agent:
+ image: timberio/vector:0.40.0-alpine
+ internalMetrics: false
diff --git a/test/e2e/testdata/podmonitor/agent-with-defaults.yaml b/test/e2e/testdata/podmonitor/agent-with-defaults.yaml
new file mode 100644
index 0000000..cf1a2d7
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/agent-with-defaults.yaml
@@ -0,0 +1,8 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: Vector
+metadata:
+ name: podmonitor-agent-defaults
+spec:
+ agent:
+ image: timberio/vector:0.40.0-alpine
+ internalMetrics: true
diff --git a/test/e2e/testdata/podmonitor/agent-with-scrape-config.yaml b/test/e2e/testdata/podmonitor/agent-with-scrape-config.yaml
new file mode 100644
index 0000000..15ad9a4
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/agent-with-scrape-config.yaml
@@ -0,0 +1,10 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: Vector
+metadata:
+ name: podmonitor-agent
+spec:
+ agent:
+ image: timberio/vector:0.40.0-alpine
+ internalMetrics: true
+ scrapeInterval: "45s"
+ scrapeTimeout: "15s"
diff --git a/test/e2e/testdata/podmonitor/agent-with-updated-interval.yaml b/test/e2e/testdata/podmonitor/agent-with-updated-interval.yaml
new file mode 100644
index 0000000..b0aed2c
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/agent-with-updated-interval.yaml
@@ -0,0 +1,10 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: Vector
+metadata:
+ name: podmonitor-agent
+spec:
+ agent:
+ image: timberio/vector:0.40.0-alpine
+ internalMetrics: true
+ scrapeInterval: "90s" # Updated from 45s to 90s
+ scrapeTimeout: "30s" # Updated from 15s to 30s
diff --git a/test/e2e/testdata/podmonitor/aggregator-with-defaults.yaml b/test/e2e/testdata/podmonitor/aggregator-with-defaults.yaml
new file mode 100644
index 0000000..fd23b10
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/aggregator-with-defaults.yaml
@@ -0,0 +1,11 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: VectorAggregator
+metadata:
+ name: podmonitor-aggregator-defaults
+spec:
+ image: timberio/vector:0.40.0-alpine
+ replicas: 1
+ internalMetrics: true
+ selector:
+ matchLabels:
+ app: test
diff --git a/test/e2e/testdata/podmonitor/aggregator-with-scrape-config.yaml b/test/e2e/testdata/podmonitor/aggregator-with-scrape-config.yaml
new file mode 100644
index 0000000..4890612
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/aggregator-with-scrape-config.yaml
@@ -0,0 +1,13 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: VectorAggregator
+metadata:
+ name: podmonitor-aggregator
+spec:
+ image: timberio/vector:0.40.0-alpine
+ replicas: 1
+ internalMetrics: true
+ scrapeInterval: "60s"
+ scrapeTimeout: "20s"
+ selector:
+ matchLabels:
+ app: test
diff --git a/test/e2e/testdata/podmonitor/cluster-aggregator-with-defaults.yaml b/test/e2e/testdata/podmonitor/cluster-aggregator-with-defaults.yaml
new file mode 100644
index 0000000..21dcb5f
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/cluster-aggregator-with-defaults.yaml
@@ -0,0 +1,12 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: ClusterVectorAggregator
+metadata:
+ name: podmonitor-cluster-agg-defaults
+spec:
+ resourceNamespace: NAMESPACE
+ image: timberio/vector:0.40.0-alpine
+ replicas: 1
+ internalMetrics: true
+ selector:
+ matchLabels:
+ app: cluster-test
diff --git a/test/e2e/testdata/podmonitor/cluster-aggregator-with-scrape-config.yaml b/test/e2e/testdata/podmonitor/cluster-aggregator-with-scrape-config.yaml
new file mode 100644
index 0000000..4dc9137
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/cluster-aggregator-with-scrape-config.yaml
@@ -0,0 +1,14 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: ClusterVectorAggregator
+metadata:
+ name: podmonitor-cluster-agg
+spec:
+ resourceNamespace: NAMESPACE
+ image: timberio/vector:0.40.0-alpine
+ replicas: 1
+ internalMetrics: true
+ scrapeInterval: "90s"
+ scrapeTimeout: "25s"
+ selector:
+ matchLabels:
+ app: cluster-test
diff --git a/test/e2e/testdata/podmonitor/pipeline-aggregator-role.yaml b/test/e2e/testdata/podmonitor/pipeline-aggregator-role.yaml
new file mode 100644
index 0000000..9a6fc9b
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/pipeline-aggregator-role.yaml
@@ -0,0 +1,16 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: VectorPipeline
+metadata:
+ name: aggregator-test-pipeline
+ labels:
+ app: test
+spec:
+ sources:
+ http_source:
+ type: http_server
+ address: "0.0.0.0:8080"
+ sinks:
+ blackhole:
+ type: blackhole
+ inputs:
+ - http_source
diff --git a/test/e2e/testdata/podmonitor/pipeline-with-custom-exporter.yaml b/test/e2e/testdata/podmonitor/pipeline-with-custom-exporter.yaml
new file mode 100644
index 0000000..ed3db6b
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/pipeline-with-custom-exporter.yaml
@@ -0,0 +1,27 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: VectorPipeline
+metadata:
+ name: custom-exporter-pipeline
+ labels:
+ app: test
+spec:
+ sources:
+ k8s_logs:
+ type: kubernetes_logs
+ extra_label_selector: "app=test"
+ transforms:
+ log_to_metric:
+ type: log_to_metric
+ inputs:
+ - k8s_logs
+ metrics:
+ - type: counter
+ field: message
+ name: log_lines_total
+ namespace: custom
+ sinks:
+ custom_prom_exporter:
+ type: prometheus_exporter
+ inputs:
+ - log_to_metric
+ address: "0.0.0.0:9599"
diff --git a/test/e2e/testdata/podmonitor/pipeline-without-exporter.yaml b/test/e2e/testdata/podmonitor/pipeline-without-exporter.yaml
new file mode 100644
index 0000000..f80f9e8
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/pipeline-without-exporter.yaml
@@ -0,0 +1,16 @@
+apiVersion: observability.kaasops.io/v1alpha1
+kind: VectorPipeline
+metadata:
+ name: no-exporter-pipeline
+ labels:
+ app: test
+spec:
+ sources:
+ k8s_logs:
+ type: kubernetes_logs
+ extra_label_selector: "app=test"
+ sinks:
+ blackhole:
+ type: blackhole
+ inputs:
+ - k8s_logs
diff --git a/test/e2e/testdata/podmonitor/test-pod.yaml b/test/e2e/testdata/podmonitor/test-pod.yaml
new file mode 100644
index 0000000..dae0145
--- /dev/null
+++ b/test/e2e/testdata/podmonitor/test-pod.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: test-app
+ labels:
+ app: test
+spec:
+ containers:
+ - name: nginx
+ image: nginx:alpine
+ command: ["/bin/sh", "-c"]
+ args:
+ - |
+ while true; do
+ echo "Test log message from test-app"
+ sleep 5
+ done