diff --git a/processor/resourcedetectionprocessor/e2e_test.go b/processor/resourcedetectionprocessor/e2e_test.go index f79f89b54a529..54b0df002becc 100644 --- a/processor/resourcedetectionprocessor/e2e_test.go +++ b/processor/resourcedetectionprocessor/e2e_test.go @@ -431,6 +431,53 @@ func TestE2EUpcloudDetector(t *testing.T) { }, 3*time.Minute, 1*time.Second) } +// TestE2EVultrDetector tests the Vultr detector by deploying a metadata-server +// sidecar that simulates the Vultr IMDS and verifying that the resource attributes +// are correctly detected and attached to metrics. +func TestE2EVultrDetector(t *testing.T) { + var expected pmetric.Metrics + expectedFile := filepath.Join("testdata", "e2e", "vultr", "expected.yaml") + expected, err := golden.ReadMetrics(expectedFile) + require.NoError(t, err) + + k8sClient, err := k8stest.NewK8sClient(testKubeConfig) + require.NoError(t, err) + + metricsConsumer := new(consumertest.MetricsSink) + shutdownSink := startUpSink(t, metricsConsumer) + defer shutdownSink() + + testID := uuid.NewString()[:8] + collectorObjs := k8stest.CreateCollectorObjects(t, k8sClient, testID, filepath.Join(".", "testdata", "e2e", "vultr", "collector"), map[string]string{}, "") + + defer func() { + for _, obj := range collectorObjs { + require.NoErrorf(t, k8stest.DeleteObject(k8sClient, obj), "failed to delete object %s", obj.GetName()) + } + }() + + wantEntries := 10 + waitForData(t, wantEntries, metricsConsumer) + + // Uncomment to regenerate golden file + // golden.WriteMetrics(t, expectedFile+".actual", metricsConsumer.AllMetrics()[len(metricsConsumer.AllMetrics())-1]) + + require.EventuallyWithT(t, func(tt *assert.CollectT) { + assert.NoError(tt, pmetrictest.CompareMetrics(expected, metricsConsumer.AllMetrics()[len(metricsConsumer.AllMetrics())-1], + pmetrictest.IgnoreTimestamp(), + pmetrictest.IgnoreStartTimestamp(), + pmetrictest.IgnoreScopeVersion(), + pmetrictest.IgnoreResourceMetricsOrder(), + pmetrictest.IgnoreMetricsOrder(), + pmetrictest.IgnoreScopeMetricsOrder(), + pmetrictest.IgnoreMetricDataPointsOrder(), + pmetrictest.IgnoreMetricValues(), + pmetrictest.IgnoreSubsequentDataPoints("system.cpu.time"), + ), + ) + }, 3*time.Minute, 1*time.Second) +} + func replaceWithStar(_ string) string { return "*" } diff --git a/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/01-metadata-configmap.yaml b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/01-metadata-configmap.yaml new file mode 100644 index 0000000000000..17f20e268e97d --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/01-metadata-configmap.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Name }}-metadata-config + namespace: default +data: + server.py: | + import json + import os + from http.server import BaseHTTPRequestHandler, HTTPServer + from urllib.parse import urlparse + + # Vultr IMDS metadata format (see https://www.vultr.com/metadata/) + VULTR_METADATA = { + "hostname": "test-vultr-instance", + "instanceid": "12345678", + "instance-v2-id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "region": { + "regioncode": "EWR" + } + } + + class Handler(BaseHTTPRequestHandler): + def do_GET(self): + parsed = urlparse(self.path) + path = parsed.path + + # Health check endpoint + if path == "/healthz": + body = b"ok" + self.send_response(200) + self.send_header("Content-Type", "text/plain; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + # Vultr IMDS endpoint (returns full metadata as JSON) + if path == "/v1.json": + body = json.dumps(VULTR_METADATA).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + # Not found + self.send_response(404) + self.end_headers() + + def log_message(self, fmt, *args): + return + + if __name__ == "__main__": + port = int(os.environ.get("PORT", "80")) + server = HTTPServer(("", port), Handler) + server.serve_forever() diff --git a/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/02-configmap.yaml b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/02-configmap.yaml new file mode 100644 index 0000000000000..13caebe5818a4 --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/02-configmap.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Name }}-config + namespace: default +data: + relay: | + exporters: + otlp: + endpoint: {{ .HostEndpoint }}:4317 + tls: + insecure: true + extensions: + health_check: + endpoint: 0.0.0.0:13133 + processors: + resourcedetection: + detectors: [vultr] + timeout: 2s + override: false + receivers: + hostmetrics: + collection_interval: 1s + scrapers: + cpu: + service: + telemetry: + logs: + level: "debug" + extensions: + - health_check + pipelines: + metrics: + receivers: + - hostmetrics + processors: + - resourcedetection + exporters: + - otlp diff --git a/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/03-serviceaccount.yaml b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/03-serviceaccount.yaml new file mode 100644 index 0000000000000..7a9803b445b6c --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/03-serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Name }}-sa + namespace: default diff --git a/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/04-service.yaml b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/04-service.yaml new file mode 100644 index 0000000000000..94587d231bbf0 --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/04-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Name }} + namespace: default +spec: + selector: + app: {{ .Name }} + ports: + - name: health + port: 13133 + targetPort: 13133 diff --git a/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/05-deployment.yaml b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/05-deployment.yaml new file mode 100644 index 0000000000000..7214d769be5a8 --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/vultr/collector/05-deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Name }} + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Name }} + template: + metadata: + labels: + app: {{ .Name }} + spec: + serviceAccountName: {{ .Name }}-sa + initContainers: + - name: metadata-server + image: python:3.13-alpine + imagePullPolicy: IfNotPresent + restartPolicy: Always + securityContext: + runAsUser: 0 + capabilities: + drop: + - "ALL" + add: + - "NET_BIND_SERVICE" + command: + - python3 + - /scripts/server.py + env: + - name: PORT + value: "80" + ports: + - containerPort: 80 + name: metadata + startupProbe: + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 1 + periodSeconds: 1 + failureThreshold: 10 + volumeMounts: + - name: metadata-script + mountPath: /scripts + - name: setup-network + image: alpine:3.21 + imagePullPolicy: IfNotPresent + securityContext: + capabilities: + add: + - NET_ADMIN + command: + - sh + - -c + - | + apk add --no-cache iptables + iptables -t nat -A OUTPUT -d 169.254.169.254 -p tcp --dport 80 -j DNAT --to-destination 127.0.0.1:80 + echo "iptables rule added to redirect 169.254.169.254:80 -> 127.0.0.1:80" + containers: + - name: otelcol + image: otelcontribcol:latest + imagePullPolicy: Never + args: + - "--config=/conf/relay" + volumeMounts: + - name: config + mountPath: /conf + volumes: + - name: config + configMap: + name: {{ .Name }}-config + items: + - key: relay + path: relay + - name: metadata-script + configMap: + name: {{ .Name }}-metadata-config diff --git a/processor/resourcedetectionprocessor/testdata/e2e/vultr/expected.yaml b/processor/resourcedetectionprocessor/testdata/e2e/vultr/expected.yaml new file mode 100644 index 0000000000000..ed2f969413dc0 --- /dev/null +++ b/processor/resourcedetectionprocessor/testdata/e2e/vultr/expected.yaml @@ -0,0 +1,107 @@ +resourceMetrics: + - resource: + attributes: + - key: cloud.provider + value: + stringValue: vultr + - key: cloud.region + value: + stringValue: ewr + - key: host.id + value: + stringValue: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + - key: host.name + value: + stringValue: test-vultr-instance + schemaUrl: https://opentelemetry.io/schemas/1.9.0 + scopeMetrics: + - metrics: + - description: Total seconds each logical CPU spent on each mode. + name: system.cpu.time + sum: + aggregationTemporality: 2 + dataPoints: + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: idle + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: interrupt + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: nice + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: softirq + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: steal + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: system + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: user + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + - asDouble: 1.0 + attributes: + - key: cpu + value: + stringValue: cpu0 + - key: state + value: + stringValue: wait + startTimeUnixNano: "1000000" + timeUnixNano: "2000000" + isMonotonic: true + unit: s + scope: + name: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper