diff --git a/.github/workflows/configs/kind-config.yaml b/.github/workflows/configs/kind-config.yaml new file mode 100644 index 0000000000..8ef9381995 --- /dev/null +++ b/.github/workflows/configs/kind-config.yaml @@ -0,0 +1,20 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane + kubeadmConfigPatches: + - | + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" + - | + kind: KubeletConfiguration + serverTLSBootstrap: true + extraPortMappings: + - containerPort: 80 + hostPort: 80 + protocol: TCP + - containerPort: 443 + hostPort: 443 + protocol: TCP diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 4019e98cdd..102571333f 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -399,7 +399,7 @@ jobs: id: get-matrix-k8s run: | includes="" - for service in "envoy"; do + for service in "envoy" "istio"; do for arch in "amd64"; do includes="${includes},{\"SERVICE\": \"${service}\", \"ARCH\": \"${arch}\"}" done @@ -425,9 +425,13 @@ jobs: node_image: kindest/node:v1.30.0 kubectl_version: v1.30.0 cluster_name: kind + config: ./.github/workflows/configs/kind-config.yaml - name: Deploy service under test + if: ${{ matrix.SERVICE != 'istio' }} run: | - kubectl apply -f k8s/${{ matrix.SERVICE }}/*.yaml + for f in k8s/${{ matrix.SERVICE }}/*.sh; do + bash "$f" + done - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} @@ -436,6 +440,9 @@ jobs: with: name: docker-otelcol-${{ matrix.ARCH }} path: ./docker-otelcol/${{ matrix.ARCH }} + - name: Fix kubelet TLS server certificates + run: | + kubectl get csr -o=jsonpath='{range.items[?(@.spec.signerName=="kubernetes.io/kubelet-serving")]}{.metadata.name}{" "}{end}' | xargs kubectl certificate approve - run: docker load -i ./docker-otelcol/${{ matrix.ARCH }}/image.tar - name: Load Docker image in kind run: | @@ -445,5 +452,5 @@ jobs: - name: Print logs if: failure() run: | - kubectl get pods - kubectl logs $(kubectl get pod -l app=otelcol -o jsonpath="{.items[0].metadata.name}") + kubectl get pods -A + kubectl get pod -A -l app=otelcol -o jsonpath="{range .items[*]}{.metadata.namespace} {.metadata.name}{'\n'}{end}" | xargs -r -n2 sh -c 'kubectl logs -n $0 $1' diff --git a/.gitignore b/.gitignore index 779d5f0bf6..4f42a88a69 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ tests/installation/testdata/systemd/splunk-otel-collector.conf # For convenience excluding sarif files generated by ./.github/workflows/scripts/govulncheck-run.sh /govulncheck/ + +# temp istio installation files +tests/receivers/istio/istio-*/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5046097e86..bf37783c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ releases where appropriate. ### 💡 Enhancements 💡 +- (Splunk) Add a new discovery bundle for Istio metrics which includes proxy, gateway, and pilot/istiod ([#5854](https://github.com/signalfx/splunk-otel-collector/pull/5854)) + - This discovery receiver, named prometheus/istio, is disabled by default. Users can enable it by setting the discovery config `splunk.discovery.receivers.prometheus/istio.enabled=true`. - (Splunk) Update `splunk-otel-javaagent` to `v2.14.0` ([#6000](https://github.com/signalfx/splunk-otel-collector/pull/6000)) - (Splunk) Update `jmx-metric-gatherer` to `v1.45.0` ([#5995](https://github.com/signalfx/splunk-otel-collector/pull/5995)) - (Splunk) Use direct connection for MongoDB discovery ([#6042](https://github.com/signalfx/splunk-otel-collector/pull/6042)) diff --git a/Makefile b/Makefile index 23479a2fe9..86d827c7cf 100644 --- a/Makefile +++ b/Makefile @@ -130,6 +130,10 @@ smartagent-integration-test: integration-test-envoy-discovery-k8s: @set -e; cd tests && $(GOTEST_SERIAL) $(BUILD_INFO_TESTS) --tags=discovery_integration_envoy_k8s -v -timeout 5m -count 1 ./... +.PHONY: integration-test-istio-discovery-k8s +integration-test-istio-discovery-k8s: + @set -e; cd tests && $(GOTEST_SERIAL) $(BUILD_INFO_TESTS) --tags=discovery_integration_istio_k8s -v -timeout 15m -count 1 ./... + .PHONY: gotest-with-codecov gotest-with-cover: @$(MAKE) for-all-target TARGET="test-with-codecov" diff --git a/cmd/otelcol/config/collector/config.d.linux/receivers/istio.discovery.yaml b/cmd/otelcol/config/collector/config.d.linux/receivers/istio.discovery.yaml new file mode 100644 index 0000000000..f416f82e02 --- /dev/null +++ b/cmd/otelcol/config/collector/config.d.linux/receivers/istio.discovery.yaml @@ -0,0 +1,110 @@ +##################################################################################### +# This file is generated by the Splunk Distribution of the OpenTelemetry Collector. # +# # +# It reflects the default configuration bundled in the Collector executable for use # +# in discovery mode (--discovery) and is provided for reference or customization. # +# Please note that any changes made to this file will need to be reconciled during # +# upgrades of the Collector. # +##################################################################################### +# prometheus/istio: +# enabled: false +# rule: +# k8s_observer: type == "pod" and ("istio.io/rev" in annotations or labels["istio"] == "pilot" or name matches "istio.*") +# config: +# default: +# config: +# scrape_configs: +# - job_name: 'istio' +# metrics_path: '`"prometheus.io/path" in annotations ? annotations["prometheus.io/path"] : "/metrics"`' +# scrape_interval: 10s +# static_configs: +# - targets: ['`endpoint`:`"prometheus.io/port" in annotations ? annotations["prometheus.io/port"] : 15090`'] +# metric_relabel_configs: +# - source_labels: [__name__] +# action: keep +# regex: "(envoy_cluster_lb_healthy_panic|\ +# envoy_cluster_manager_warming_clusters|\ +# envoy_cluster_membership_healthy|\ +# envoy_cluster_membership_total|\ +# envoy_cluster_ssl_handshake|\ +# envoy_cluster_ssl_session_reused|\ +# envoy_cluster_ssl_versions_TLSv1_2|\ +# envoy_cluster_ssl_versions_TLSv1_3|\ +# envoy_cluster_upstream_cx_active|\ +# envoy_cluster_upstream_cx_close_notify|\ +# envoy_cluster_upstream_cx_connect_attempts_exceeded|\ +# envoy_cluster_upstream_cx_connect_ms_sum|\ +# envoy_cluster_upstream_cx_connect_timeout|\ +# envoy_cluster_upstream_cx_destroy_local_with_active_rq|\ +# envoy_cluster_upstream_cx_http1_total|\ +# envoy_cluster_upstream_cx_http2_total|\ +# envoy_cluster_upstream_cx_idle_timeout|\ +# envoy_cluster_upstream_cx_max_requests|\ +# envoy_cluster_upstream_cx_none_healthy|\ +# envoy_cluster_upstream_cx_pool_overflow|\ +# envoy_cluster_upstream_cx_protocol_error|\ +# envoy_cluster_upstream_cx_total|\ +# envoy_cluster_upstream_rq_4xx|\ +# envoy_cluster_upstream_rq_5xx|\ +# envoy_cluster_upstream_rq_active|\ +# envoy_cluster_upstream_rq_cancelled|\ +# envoy_cluster_upstream_rq_completed|\ +# envoy_cluster_upstream_rq_pending_active|\ +# envoy_cluster_upstream_rq_retry|\ +# envoy_cluster_upstream_rq_retry_limit_exceeded|\ +# envoy_cluster_upstream_rq_timeout|\ +# envoy_cluster_upstream_rq_tx_reset|\ +# envoy_cluster_upstream_rq_time|\ +# envoy_cluster_upstream_rq_xx|\ +# envoy_listener_downstream_cx_total|\ +# envoy_listener_ssl_versions_TLSv1_2|\ +# envoy_listener_ssl_versions_TLSv1_3|\ +# envoy_server_live|\ +# envoy_server_memory_allocated|\ +# envoy_server_memory_heap_size|\ +# envoy_server_total_connections|\ +# envoy_server_uptime|\ +# istio_mesh_connections_from_logs|\ +# istio_monitor_pods_without_sidecars|\ +# istio_request_bytes|\ +# istio_request_duration_milliseconds|\ +# istio_request_messages_total|\ +# istio_requests_total|\ +# istio_response_messages_total|\ +# istio_tcp_connections_closed_total|\ +# istio_tcp_connections_opened_total|\ +# istio_tcp_received_bytes_total|\ +# istio_tcp_response_bytes_total|\ +# pilot_conflict_inbound_listener|\ +# pilot_eds_no_instances|\ +# pilot_k8s_cfg_events|\ +# pilot_k8s_endpoints_pending_pod|\ +# pilot_k8s_endpoints_with_no_pods|\ +# pilot_no_ip|\ +# pilot_proxy_convergence_time|\ +# pilot_proxy_queue_time|\ +# pilot_services|\ +# pilot_xds_cds_reject|\ +# pilot_xds_eds_reject|\ +# pilot_xds_expired_nonce|\ +# pilot_xds_lds_reject|\ +# pilot_xds_push_context_errors|\ +# pilot_xds_push_time|\ +# pilot_xds_rds_reject|\ +# pilot_xds_send_time|\ +# pilot_xds_write_timeout)(?:_sum|_count|_bucket)?" +# status: +# metrics: +# - status: successful +# strict: envoy_server_uptime +# message: istio prometheus receiver is working for istio-proxy! +# - status: successful +# strict: pilot_services +# message: istio prometheus receiver is working for istiod! +# statements: +# - status: failed +# regexp: "connection refused" +# message: The container is not serving http connections. +# - status: failed +# regexp: "dial tcp: lookup" +# message: Unable to resolve istio prometheus tcp endpoint diff --git a/internal/confmapprovider/discovery/bundle/bundle.d/receivers/istio.discovery.yaml b/internal/confmapprovider/discovery/bundle/bundle.d/receivers/istio.discovery.yaml new file mode 100644 index 0000000000..277dcb0ff9 --- /dev/null +++ b/internal/confmapprovider/discovery/bundle/bundle.d/receivers/istio.discovery.yaml @@ -0,0 +1,106 @@ +##################################################################################### +# Do not edit manually! # +# All changes must be made to associated .tmpl file before running 'make bundle.d'. # +##################################################################################### +prometheus/istio: + enabled: false + rule: + k8s_observer: type == "pod" and ("istio.io/rev" in annotations or labels["istio"] == "pilot" or name matches "istio.*") + config: + default: + config: + scrape_configs: + - job_name: 'istio' + metrics_path: '`"prometheus.io/path" in annotations ? annotations["prometheus.io/path"] : "/metrics"`' + scrape_interval: 10s + static_configs: + - targets: ['`endpoint`:`"prometheus.io/port" in annotations ? annotations["prometheus.io/port"] : 15090`'] + metric_relabel_configs: + - source_labels: [__name__] + action: keep + regex: "(envoy_cluster_lb_healthy_panic|\ + envoy_cluster_manager_warming_clusters|\ + envoy_cluster_membership_healthy|\ + envoy_cluster_membership_total|\ + envoy_cluster_ssl_handshake|\ + envoy_cluster_ssl_session_reused|\ + envoy_cluster_ssl_versions_TLSv1_2|\ + envoy_cluster_ssl_versions_TLSv1_3|\ + envoy_cluster_upstream_cx_active|\ + envoy_cluster_upstream_cx_close_notify|\ + envoy_cluster_upstream_cx_connect_attempts_exceeded|\ + envoy_cluster_upstream_cx_connect_ms_sum|\ + envoy_cluster_upstream_cx_connect_timeout|\ + envoy_cluster_upstream_cx_destroy_local_with_active_rq|\ + envoy_cluster_upstream_cx_http1_total|\ + envoy_cluster_upstream_cx_http2_total|\ + envoy_cluster_upstream_cx_idle_timeout|\ + envoy_cluster_upstream_cx_max_requests|\ + envoy_cluster_upstream_cx_none_healthy|\ + envoy_cluster_upstream_cx_pool_overflow|\ + envoy_cluster_upstream_cx_protocol_error|\ + envoy_cluster_upstream_cx_total|\ + envoy_cluster_upstream_rq_4xx|\ + envoy_cluster_upstream_rq_5xx|\ + envoy_cluster_upstream_rq_active|\ + envoy_cluster_upstream_rq_cancelled|\ + envoy_cluster_upstream_rq_completed|\ + envoy_cluster_upstream_rq_pending_active|\ + envoy_cluster_upstream_rq_retry|\ + envoy_cluster_upstream_rq_retry_limit_exceeded|\ + envoy_cluster_upstream_rq_timeout|\ + envoy_cluster_upstream_rq_tx_reset|\ + envoy_cluster_upstream_rq_time|\ + envoy_cluster_upstream_rq_xx|\ + envoy_listener_downstream_cx_total|\ + envoy_listener_ssl_versions_TLSv1_2|\ + envoy_listener_ssl_versions_TLSv1_3|\ + envoy_server_live|\ + envoy_server_memory_allocated|\ + envoy_server_memory_heap_size|\ + envoy_server_total_connections|\ + envoy_server_uptime|\ + istio_mesh_connections_from_logs|\ + istio_monitor_pods_without_sidecars|\ + istio_request_bytes|\ + istio_request_duration_milliseconds|\ + istio_request_messages_total|\ + istio_requests_total|\ + istio_response_messages_total|\ + istio_tcp_connections_closed_total|\ + istio_tcp_connections_opened_total|\ + istio_tcp_received_bytes_total|\ + istio_tcp_response_bytes_total|\ + pilot_conflict_inbound_listener|\ + pilot_eds_no_instances|\ + pilot_k8s_cfg_events|\ + pilot_k8s_endpoints_pending_pod|\ + pilot_k8s_endpoints_with_no_pods|\ + pilot_no_ip|\ + pilot_proxy_convergence_time|\ + pilot_proxy_queue_time|\ + pilot_services|\ + pilot_xds_cds_reject|\ + pilot_xds_eds_reject|\ + pilot_xds_expired_nonce|\ + pilot_xds_lds_reject|\ + pilot_xds_push_context_errors|\ + pilot_xds_push_time|\ + pilot_xds_rds_reject|\ + pilot_xds_send_time|\ + pilot_xds_write_timeout)(?:_sum|_count|_bucket)?" + status: + metrics: + - status: successful + strict: envoy_server_uptime + message: istio prometheus receiver is working for istio-proxy! + - status: successful + strict: pilot_services + message: istio prometheus receiver is working for istiod! + statements: + - status: failed + regexp: "connection refused" + message: The container is not serving http connections. + - status: failed + regexp: "dial tcp: lookup" + message: Unable to resolve istio prometheus tcp endpoint diff --git a/internal/confmapprovider/discovery/bundle/bundle.d/receivers/istio.discovery.yaml.tmpl b/internal/confmapprovider/discovery/bundle/bundle.d/receivers/istio.discovery.yaml.tmpl new file mode 100644 index 0000000000..83280efdc0 --- /dev/null +++ b/internal/confmapprovider/discovery/bundle/bundle.d/receivers/istio.discovery.yaml.tmpl @@ -0,0 +1,102 @@ +{{ receiver "prometheus/istio" }}: + enabled: false + rule: + k8s_observer: type == "pod" and ("istio.io/rev" in annotations or labels["istio"] == "pilot" or name matches "istio.*") + config: + default: + config: + scrape_configs: + - job_name: 'istio' + metrics_path: '`"prometheus.io/path" in annotations ? annotations["prometheus.io/path"] : "/metrics"`' + scrape_interval: 10s + static_configs: + - targets: ['`endpoint`:`"prometheus.io/port" in annotations ? annotations["prometheus.io/port"] : 15090`'] + metric_relabel_configs: + - source_labels: [__name__] + action: keep + regex: "(envoy_cluster_lb_healthy_panic|\ + envoy_cluster_manager_warming_clusters|\ + envoy_cluster_membership_healthy|\ + envoy_cluster_membership_total|\ + envoy_cluster_ssl_handshake|\ + envoy_cluster_ssl_session_reused|\ + envoy_cluster_ssl_versions_TLSv1_2|\ + envoy_cluster_ssl_versions_TLSv1_3|\ + envoy_cluster_upstream_cx_active|\ + envoy_cluster_upstream_cx_close_notify|\ + envoy_cluster_upstream_cx_connect_attempts_exceeded|\ + envoy_cluster_upstream_cx_connect_ms_sum|\ + envoy_cluster_upstream_cx_connect_timeout|\ + envoy_cluster_upstream_cx_destroy_local_with_active_rq|\ + envoy_cluster_upstream_cx_http1_total|\ + envoy_cluster_upstream_cx_http2_total|\ + envoy_cluster_upstream_cx_idle_timeout|\ + envoy_cluster_upstream_cx_max_requests|\ + envoy_cluster_upstream_cx_none_healthy|\ + envoy_cluster_upstream_cx_pool_overflow|\ + envoy_cluster_upstream_cx_protocol_error|\ + envoy_cluster_upstream_cx_total|\ + envoy_cluster_upstream_rq_4xx|\ + envoy_cluster_upstream_rq_5xx|\ + envoy_cluster_upstream_rq_active|\ + envoy_cluster_upstream_rq_cancelled|\ + envoy_cluster_upstream_rq_completed|\ + envoy_cluster_upstream_rq_pending_active|\ + envoy_cluster_upstream_rq_retry|\ + envoy_cluster_upstream_rq_retry_limit_exceeded|\ + envoy_cluster_upstream_rq_timeout|\ + envoy_cluster_upstream_rq_tx_reset|\ + envoy_cluster_upstream_rq_time|\ + envoy_cluster_upstream_rq_xx|\ + envoy_listener_downstream_cx_total|\ + envoy_listener_ssl_versions_TLSv1_2|\ + envoy_listener_ssl_versions_TLSv1_3|\ + envoy_server_live|\ + envoy_server_memory_allocated|\ + envoy_server_memory_heap_size|\ + envoy_server_total_connections|\ + envoy_server_uptime|\ + istio_mesh_connections_from_logs|\ + istio_monitor_pods_without_sidecars|\ + istio_request_bytes|\ + istio_request_duration_milliseconds|\ + istio_request_messages_total|\ + istio_requests_total|\ + istio_response_messages_total|\ + istio_tcp_connections_closed_total|\ + istio_tcp_connections_opened_total|\ + istio_tcp_received_bytes_total|\ + istio_tcp_response_bytes_total|\ + pilot_conflict_inbound_listener|\ + pilot_eds_no_instances|\ + pilot_k8s_cfg_events|\ + pilot_k8s_endpoints_pending_pod|\ + pilot_k8s_endpoints_with_no_pods|\ + pilot_no_ip|\ + pilot_proxy_convergence_time|\ + pilot_proxy_queue_time|\ + pilot_services|\ + pilot_xds_cds_reject|\ + pilot_xds_eds_reject|\ + pilot_xds_expired_nonce|\ + pilot_xds_lds_reject|\ + pilot_xds_push_context_errors|\ + pilot_xds_push_time|\ + pilot_xds_rds_reject|\ + pilot_xds_send_time|\ + pilot_xds_write_timeout)(?:_sum|_count|_bucket)?" + status: + metrics: + - status: successful + strict: envoy_server_uptime + message: istio prometheus receiver is working for istio-proxy! + - status: successful + strict: pilot_services + message: istio prometheus receiver is working for istiod! + statements: + - status: failed + regexp: "connection refused" + message: The container is not serving http connections. + - status: failed + regexp: "dial tcp: lookup" + message: Unable to resolve istio prometheus tcp endpoint diff --git a/internal/confmapprovider/discovery/bundle/bundle_gen.go b/internal/confmapprovider/discovery/bundle/bundle_gen.go index 12c2b77e66..3c6586dc27 100644 --- a/internal/confmapprovider/discovery/bundle/bundle_gen.go +++ b/internal/confmapprovider/discovery/bundle/bundle_gen.go @@ -27,6 +27,8 @@ //go:generate discoverybundler --render --commented --dir ../../../../cmd/otelcol/config/collector/config.d.linux/receivers -t bundle.d/receivers/apache.discovery.yaml.tmpl //go:generate discoverybundler --render --template bundle.d/receivers/envoy.discovery.yaml.tmpl //go:generate discoverybundler --render --commented --dir ../../../../cmd/otelcol/config/collector/config.d.linux/receivers -t bundle.d/receivers/envoy.discovery.yaml.tmpl +//go:generate discoverybundler --render --template bundle.d/receivers/istio.discovery.yaml.tmpl +//go:generate discoverybundler --render --commented --dir ../../../../cmd/otelcol/config/collector/config.d.linux/receivers -t bundle.d/receivers/istio.discovery.yaml.tmpl //go:generate discoverybundler --render --template bundle.d/receivers/jmx-cassandra.discovery.yaml.tmpl //go:generate discoverybundler --render --commented --dir ../../../../cmd/otelcol/config/collector/config.d.linux/receivers -t bundle.d/receivers/jmx-cassandra.discovery.yaml.tmpl //go:generate discoverybundler --render --template bundle.d/receivers/kafkametrics.discovery.yaml.tmpl diff --git a/internal/confmapprovider/discovery/bundle/bundledfs_other_test.go b/internal/confmapprovider/discovery/bundle/bundledfs_other_test.go index 4914ac06a3..fd2be3c3ce 100644 --- a/internal/confmapprovider/discovery/bundle/bundledfs_other_test.go +++ b/internal/confmapprovider/discovery/bundle/bundledfs_other_test.go @@ -29,6 +29,7 @@ func TestBundleDir(t *testing.T) { require.Equal(t, []string{ "bundle.d/receivers/apache.discovery.yaml", "bundle.d/receivers/envoy.discovery.yaml", + "bundle.d/receivers/istio.discovery.yaml", "bundle.d/receivers/jmx-cassandra.discovery.yaml", "bundle.d/receivers/kafkametrics.discovery.yaml", "bundle.d/receivers/mongodb.discovery.yaml", diff --git a/internal/confmapprovider/discovery/bundle/bundledfs_others.go b/internal/confmapprovider/discovery/bundle/bundledfs_others.go index 292f6aaf50..9b9ae24aea 100644 --- a/internal/confmapprovider/discovery/bundle/bundledfs_others.go +++ b/internal/confmapprovider/discovery/bundle/bundledfs_others.go @@ -27,6 +27,7 @@ import ( //go:embed bundle.d/extensions/k8s-observer.discovery.yaml //go:embed bundle.d/receivers/apache.discovery.yaml //go:embed bundle.d/receivers/envoy.discovery.yaml +//go:embed bundle.d/receivers/istio.discovery.yaml //go:embed bundle.d/receivers/jmx-cassandra.discovery.yaml //go:embed bundle.d/receivers/kafkametrics.discovery.yaml //go:embed bundle.d/receivers/mongodb.discovery.yaml diff --git a/internal/confmapprovider/discovery/bundle/bundledfs_windows.go b/internal/confmapprovider/discovery/bundle/bundledfs_windows.go index aa7c83298c..b973a35d1b 100644 --- a/internal/confmapprovider/discovery/bundle/bundledfs_windows.go +++ b/internal/confmapprovider/discovery/bundle/bundledfs_windows.go @@ -27,6 +27,7 @@ import ( //go:embed bundle.d/extensions/k8s-observer.discovery.yaml //go:embed bundle.d/receivers/apache.discovery.yaml //go:embed bundle.d/receivers/envoy.discovery.yaml +//go:embed bundle.d/receivers/istio.discovery.yaml //go:embed bundle.d/receivers/jmx-cassandra.discovery.yaml //go:embed bundle.d/receivers/kafkametrics.discovery.yaml //go:embed bundle.d/receivers/mongodb.discovery.yaml diff --git a/internal/confmapprovider/discovery/bundle/bundledfs_windows_test.go b/internal/confmapprovider/discovery/bundle/bundledfs_windows_test.go index 49d1604198..cb59689639 100644 --- a/internal/confmapprovider/discovery/bundle/bundledfs_windows_test.go +++ b/internal/confmapprovider/discovery/bundle/bundledfs_windows_test.go @@ -29,6 +29,7 @@ func TestBundleDir(t *testing.T) { require.Equal(t, []string{ "bundle.d/receivers/apache.discovery.yaml", "bundle.d/receivers/envoy.discovery.yaml", + "bundle.d/receivers/istio.discovery.yaml", "bundle.d/receivers/jmx-cassandra.discovery.yaml", "bundle.d/receivers/kafkametrics.discovery.yaml", "bundle.d/receivers/mongodb.discovery.yaml", diff --git a/internal/confmapprovider/discovery/bundle/components.go b/internal/confmapprovider/discovery/bundle/components.go index 05bc6d5f16..967fc81b64 100644 --- a/internal/confmapprovider/discovery/bundle/components.go +++ b/internal/confmapprovider/discovery/bundle/components.go @@ -33,6 +33,7 @@ var ( receivers = []string{ "apache", "envoy", + "istio", "jmx-cassandra", "kafkametrics", "mongodb", @@ -68,6 +69,7 @@ var ( windows := map[string]struct{}{ "apache": {}, "envoy": {}, + "istio": {}, "jmx-cassandra": {}, "kafkametrics": {}, "mongodb": {}, diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000000..c5fb071b16 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,6 @@ +This folder contains a set of scripts that execute in separate runs to create Kubernetes environments where a specific +integration test will run to test discovery. + +The folder name (e.g. `envoy`) will match the equivalent make target (e.g. `make integration-test-envoy-discovery-k8s`). + +Reference: `.github/workflows/integration-test.yaml`. diff --git a/k8s/envoy/envoy.sh b/k8s/envoy/envoy.sh new file mode 100644 index 0000000000..c711df6e5c --- /dev/null +++ b/k8s/envoy/envoy.sh @@ -0,0 +1,3 @@ +#!/bin/bash +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +kubectl apply -f $SCRIPT_DIR/envoy.yaml diff --git a/tests/go.mod b/tests/go.mod index 56fc325eeb..c28456a34f 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/uuid v1.6.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.122.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.122.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/xk8stest v0.122.0 github.com/shirou/gopsutil/v3 v3.24.5 github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.35.0 @@ -126,9 +127,10 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.20.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.37.0 // indirect diff --git a/tests/go.sum b/tests/go.sum index b04295f229..2dd7855fb8 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -167,8 +167,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -309,6 +309,8 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.122.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.122.0/go.mod h1:45Di232vetvGjROIPxlBlyBMBAgA95szYP8du09shDE= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.122.0 h1:Jsn9I74nG85Iw7wWET6g0eQ9tbwVndgNHbzHqdlZVqI= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.122.0/go.mod h1:BpcyQo7MedcfxlBmIgRB5DxdLlEa0wHRJ/Nhe8jjnW4= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/xk8stest v0.122.0 h1:xvtPcPIZ+B7CTzuEir/DQeevoYqYwNnWnKcVOF+VKt8= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/xk8stest v0.122.0/go.mod h1:w7Ieb1Qi3mz86JpYnB4a2ChBR/F+qawXlknzDq/nVao= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -474,10 +476,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRND go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.20.0 h1:CsBiKCiQPdSjS+MlRiqeTI9JDDpSuk0Hb6QTRfwer8k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.20.0/go.mod h1:CMJYNAfooOwSZSAmAeMUV1M+TXld3BiK++z9fqIm2xk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= @@ -486,8 +488,8 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= diff --git a/tests/internal/discoverytest/discoverytest.go b/tests/internal/discoverytest/discoverytest.go index 95931d0cf1..5546977deb 100644 --- a/tests/internal/discoverytest/discoverytest.go +++ b/tests/internal/discoverytest/discoverytest.go @@ -20,12 +20,17 @@ import ( "context" "fmt" "os" + "path" + "path/filepath" + "runtime" "syscall" "testing" "time" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" + docker "github.com/docker/docker/client" + k8stest "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/xk8stest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" @@ -33,6 +38,8 @@ import ( "go.opentelemetry.io/collector/consumer/consumertest" "go.opentelemetry.io/collector/receiver/otlpreceiver" "go.opentelemetry.io/collector/receiver/receivertest" + + "github.com/signalfx/splunk-otel-collector/tests/testutils" ) const ( @@ -41,19 +48,23 @@ const ( otelEntityAttributesAttr = "otel.entity.attributes" ) -func Run(t *testing.T, receiverName string, configFilePath string, logMessageToAssert string) { - factory := otlpreceiver.NewFactory() - port := 16745 - cfg := factory.CreateDefaultConfig().(*otlpreceiver.Config) - cfg.GRPC.NetAddr.Endpoint = fmt.Sprintf("localhost:%d", port) - endpoint := cfg.GRPC.NetAddr.Endpoint +func setupReceiver(t *testing.T, endpoint string) *consumertest.LogsSink { + f := otlpreceiver.NewFactory() + cfg := f.CreateDefaultConfig().(*otlpreceiver.Config) + cfg.GRPC.NetAddr.Endpoint = endpoint sink := &consumertest.LogsSink{} - receiver, err := factory.CreateLogs(context.Background(), receivertest.NewNopSettings(factory.Type()), cfg, sink) + receiver, err := f.CreateLogs(context.Background(), receivertest.NewNopSettings(f.Type()), cfg, sink) require.NoError(t, err) require.NoError(t, receiver.Start(context.Background(), componenttest.NewNopHost())) t.Cleanup(func() { require.NoError(t, receiver.Shutdown(context.Background())) }) + return sink +} +func Run(t *testing.T, receiverName string, configFilePath string, logMessageToAssert string) { + port := 16745 + endpoint := fmt.Sprintf("localhost:%d", port) + sink := setupReceiver(t, endpoint) dockerGID, err := getDockerGID() require.NoError(t, err) @@ -123,7 +134,103 @@ func Run(t *testing.T, receiverName string, configFilePath string, logMessageToA t.Cleanup(func() { require.NoError(t, c.Terminate(context.Background())) }) +} + +// RunWithK8s create a collector in an existing k8s cluster (set env KUBECONFIG for access to cluster) +// and assert that all expectedEntityAttrs for discovered entities are received by the collector in k8s. +func RunWithK8s(t *testing.T, expectedEntityAttrs []map[string]string, setDiscoveryArgs []string) { + kubeConfig := os.Getenv("KUBECONFIG") + if kubeConfig == "" { + t.Fatal("KUBECONFIG environment variable not set") + } + + skipTearDown := false + if os.Getenv("SKIP_TEARDOWN") == "true" { + skipTearDown = true + } + + port := int(testutils.GetAvailablePort(t)) + endpoint := fmt.Sprintf("0.0.0.0:%d", port) + sink := setupReceiver(t, endpoint) + + k8sClient, err := k8stest.NewK8sClient(kubeConfig) + require.NoError(t, err) + + dockerHost := getHostEndpoint(t) + _, filename, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("Failed to get current file directory") + } + currentDir := path.Dir(filename) + + objs, err := k8stest.CreateObjects(k8sClient, filepath.Join(currentDir, "k8s")) + require.NoError(t, err) + t.Cleanup(func() { + if skipTearDown { + return + } + for _, obj := range objs { + require.NoErrorf(t, k8stest.DeleteObject(k8sClient, obj), "failed to delete object %s", obj.GetName()) + } + }) + + var extraDiscoveryArgs string + for _, arg := range setDiscoveryArgs { + extraDiscoveryArgs += fmt.Sprintf(" - --set=%s\n", arg) + } + + collectorObjs := k8stest.CreateCollectorObjects(t, k8sClient, "test", filepath.Join(currentDir, "k8s", "collector"), map[string]string{"ExtraDiscoveryArgs": extraDiscoveryArgs}, fmt.Sprintf("%s:%d", dockerHost, port)) + t.Cleanup(func() { + if skipTearDown { + return + } + for _, obj := range collectorObjs { + require.NoErrorf(t, k8stest.DeleteObject(k8sClient, obj), "failed to delete object %s", obj.GetName()) + } + }) + collectLogsWithAttrs(t, sink, expectedEntityAttrs) +} + +func collectLogsWithAttrs(t *testing.T, sink *consumertest.LogsSink, expectedEntityAttrs []map[string]string) { + seenLogs := make(map[string]bool) + + assert.EventuallyWithT(t, func(tt *assert.CollectT) { + if len(sink.AllLogs()) == 0 { + assert.Fail(tt, "No logs collected") + return + } + for i := 0; i < len(sink.AllLogs()); i++ { + plogs := sink.AllLogs()[i] + lrs := plogs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords() + for j := 0; j < lrs.Len(); j++ { + lr := lrs.At(j) + attrMap, ok := lr.Attributes().Get(otelEntityAttributesAttr) + if ok { + m := attrMap.Map() + for _, expectedLog := range expectedEntityAttrs { + matches := true + for key, value := range expectedLog { + attrValue, ok := m.Get(key) + if !ok || attrValue.AsString() != value { + matches = false + break + } + } + if matches { + seenLogs[fmt.Sprintf("%v", expectedLog)] = true + } + } + } + } + } + for _, expectedLog := range expectedEntityAttrs { + assert.True(tt, seenLogs[fmt.Sprintf("%v", expectedLog)], "Did not see expected log: %v", expectedLog) + } + if len(seenLogs) == len(expectedEntityAttrs) { + t.Log("Successfully matched all expected logs") + } + }, 10*time.Minute, 10*time.Second, "Did not get expected logs in time") } func getDockerGID() (string, error) { @@ -139,3 +246,28 @@ func getDockerGID() (string, error) { dockerGID := fmt.Sprintf("%d", stat.Gid) return dockerGID, nil } + +// getHostEndpoint returns the docker host endpoint. +func getHostEndpoint(t *testing.T) string { + if host, ok := os.LookupEnv("HOST_ENDPOINT"); ok { + return host + } + if runtime.GOOS == "darwin" { + return "host.docker.internal" + } + + client, err := docker.NewClientWithOpts(docker.FromEnv) + require.NoError(t, err) + client.NegotiateAPIVersion(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + network, err := client.NetworkInspect(ctx, "kind", network.InspectOptions{}) + require.NoError(t, err) + for _, ipam := range network.IPAM.Config { + if ipam.Gateway != "" { + return ipam.Gateway + } + } + require.Fail(t, "failed to find host endpoint") + return "" +} diff --git a/tests/internal/discoverytest/k8s/collector/0-serviceAccount.yaml b/tests/internal/discoverytest/k8s/collector/0-serviceAccount.yaml new file mode 100644 index 0000000000..40e16f759d --- /dev/null +++ b/tests/internal/discoverytest/k8s/collector/0-serviceAccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +automountServiceAccountToken: true +metadata: + name: otelcol + namespace: test + labels: + app: otelcol diff --git a/tests/internal/discoverytest/k8s/collector/1-clusterRole.yaml b/tests/internal/discoverytest/k8s/collector/1-clusterRole.yaml new file mode 100644 index 0000000000..6a384e104c --- /dev/null +++ b/tests/internal/discoverytest/k8s/collector/1-clusterRole.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: otelcol + labels: + app: otelcol +rules: + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch diff --git a/tests/internal/discoverytest/k8s/collector/2-clusterRoleBinding.yaml b/tests/internal/discoverytest/k8s/collector/2-clusterRoleBinding.yaml new file mode 100644 index 0000000000..409bc05e01 --- /dev/null +++ b/tests/internal/discoverytest/k8s/collector/2-clusterRoleBinding.yaml @@ -0,0 +1,14 @@ +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: otelcol + labels: + app: otelcol +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: otelcol +subjects: + - kind: ServiceAccount + name: otelcol + namespace: test diff --git a/tests/internal/discoverytest/k8s/collector/3-config.yaml b/tests/internal/discoverytest/k8s/collector/3-config.yaml new file mode 100644 index 0000000000..1035c98791 --- /dev/null +++ b/tests/internal/discoverytest/k8s/collector/3-config.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: otelcol-config + namespace: test +data: + config.yaml: | + exporters: + otlp: + endpoint: {{ .HostEndpoint }} + tls: + insecure: true + debug: + verbosity: detailed + service: + telemetry: + logs: + level: debug + pipelines: + logs/entities: + exporters: + - otlp + metrics: + exporters: + - otlp diff --git a/tests/internal/discoverytest/k8s/collector/4-collector.yaml b/tests/internal/discoverytest/k8s/collector/4-collector.yaml new file mode 100644 index 0000000000..2e24874e6c --- /dev/null +++ b/tests/internal/discoverytest/k8s/collector/4-collector.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: otelcol + namespace: test +spec: + replicas: 1 + selector: + matchLabels: + app: otelcol + template: + metadata: + name: otelcol + labels: + app: otelcol + spec: + serviceAccountName: otelcol + containers: + - image: otelcol:latest + command: + - /otelcol + - --config + - /opt/config/config.yaml + - --discovery + - --set=splunk.discovery.extensions.docker_observer.enabled=false + - --set=splunk.discovery.extensions.host_observer.enabled=false +{{ .ExtraDiscoveryArgs }} + name: otelcol + imagePullPolicy: IfNotPresent + volumeMounts: + - name: config-volume + mountPath: /opt/config + env: + - name: SPLUNK_DISCOVERY_DURATION + value: 20s + - name: SPLUNK_DISCOVERY_LOG_LEVEL + value: debug + volumes: + - name: config-volume + configMap: + name: otelcol-config diff --git a/tests/internal/discoverytest/k8s/namespace.yaml b/tests/internal/discoverytest/k8s/namespace.yaml new file mode 100644 index 0000000000..7c265c0193 --- /dev/null +++ b/tests/internal/discoverytest/k8s/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: test diff --git a/tests/receivers/istio/bundled_k8s_test.go b/tests/receivers/istio/bundled_k8s_test.go new file mode 100644 index 0000000000..f5990c6f4f --- /dev/null +++ b/tests/receivers/istio/bundled_k8s_test.go @@ -0,0 +1,265 @@ +// Copyright Splunk, Inc. +// +// 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. + +//go:build discovery_integration_istio_k8s + +package tests + +import ( + "archive/tar" + "compress/gzip" + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + + "github.com/signalfx/splunk-otel-collector/tests/internal/discoverytest" +) + +const ( + istioVersion = "1.24.2" +) + +func downloadIstio(t *testing.T, version string) (string, string) { + var url string + if runtime.GOOS == "darwin" { + url = fmt.Sprintf("https://github.com/istio/istio/releases/download/%s/istio-%s-osx.tar.gz", version, version) + } else if runtime.GOOS == "linux" { + url = fmt.Sprintf("https://github.com/istio/istio/releases/download/%s/istio-%s-linux-amd64.tar.gz", version, version) + } else { + t.Fatalf("unsupported operating system: %s", runtime.GOOS) + } + + resp, err := http.Get(url) + require.NoError(t, err) + defer resp.Body.Close() + + gz, err := gzip.NewReader(resp.Body) + require.NoError(t, err) + defer gz.Close() + + tr := tar.NewReader(gz) + var istioDir string + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + require.NoError(t, err) + + target := filepath.Join(".", hdr.Name) + if hdr.FileInfo().IsDir() && istioDir == "" { + istioDir = target + } + if hdr.FileInfo().IsDir() { + require.NoError(t, os.MkdirAll(target, hdr.FileInfo().Mode())) + } else { + f, err := os.Create(target) + require.NoError(t, err) + defer f.Close() + + _, err = io.Copy(f, tr) + require.NoError(t, err) + } + } + require.NotEmpty(t, istioDir, "istioctl path not found") + + absIstioDir, err := filepath.Abs(istioDir) + require.NoError(t, err, "failed to get absolute path for istioDir") + + istioctlPath := filepath.Join(absIstioDir, "bin", "istioctl") + require.FileExists(t, istioctlPath, "istioctl binary not found") + require.NoError(t, os.Chmod(istioctlPath, 0755), "failed to set executable permission for istioctl") + + t.Cleanup(func() { + os.RemoveAll(absIstioDir) + }) + + return absIstioDir, istioctlPath +} + +func TestIstioEntities(t *testing.T) { + kubeCfg := os.Getenv("KUBECONFIG") + if kubeCfg == "" { + t.Fatal("KUBECONFIG environment variable not set") + } + + skipTearDown := false + if os.Getenv("SKIP_TEARDOWN") == "true" { + skipTearDown = true + } + + config, err := clientcmd.BuildConfigFromFlags("", kubeCfg) + require.NoError(t, err) + + clientset, err := kubernetes.NewForConfig(config) + require.NoError(t, err) + + runCommand(t, "kubectl label ns default istio-injection=enabled") + + istioDir, istioctlPath := downloadIstio(t, istioVersion) + runCommand(t, fmt.Sprintf("%s install -y", istioctlPath)) + + t.Cleanup(func() { + if skipTearDown { + t.Log("Skipping teardown as SKIP_TEARDOWN is set to true") + return + } + runCommand(t, fmt.Sprintf("%s uninstall --purge -y", istioctlPath)) + }) + + // Patch ingress gateway to work in kind cluster + patchResource(t, clientset, "istio-system", "istio-ingressgateway", "deployments", `{"spec":{"template":{"spec":{"containers":[{"name":"istio-proxy","ports":[{"containerPort":8080,"hostPort":80},{"containerPort":8443,"hostPort":443}]}]}}}}`) + patchResource(t, clientset, "istio-system", "istio-ingressgateway", "services", `{"spec": {"type": "ClusterIP"}}`) + + demoYaml := filepath.Join(istioDir, "samples", "bookinfo", "platform", "kube", "bookinfo.yaml") + runCommand(t, fmt.Sprintf("kubectl apply -f %s", demoYaml)) + t.Cleanup(func() { + if skipTearDown { + return + } + runCommand(t, fmt.Sprintf("kubectl delete -f %s", demoYaml)) + }) + + waitForPodsReady(t, clientset, "") + + pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{}) + require.NoError(t, err) + + expectedLogs := []map[string]string{} + for _, pod := range pods.Items { + message := "istio prometheus receiver is working for istio-proxy!" + if pod.Labels["app"] == "istiod" { + message = "istio prometheus receiver is working for istiod!" + } + if pod.Labels["app"] == "istiod" || pod.Labels["istio"] == "ingressgateway" || hasIstioProxyContainer(pod) { + t.Logf("Matching pod: %s", pod.Name) + podIP := pod.Status.PodIP + promPort := "15090" + if port, ok := pod.Annotations["prometheus.io/port"]; ok { + promPort = port + } + expectedLogs = append(expectedLogs, map[string]string{ + "discovery.receiver.name": "istio", + "discovery.receiver.type": "prometheus", + "k8s.pod.name": pod.Name, + "k8s.namespace.name": pod.Namespace, + "discovery.message": message, + "discovery.observer.type": "k8s_observer", + "discovery.status": "successful", + "endpoint": podIP, + "net.host.name": podIP, + "net.host.port": promPort, + "server.address": podIP, + "server.port": promPort, + "service.instance.id": fmt.Sprintf("%s:%s", podIP, promPort), + "service.name": "istio", + "service.type": "prometheus", + "type": "pod", + "url.scheme": "http", + }) + } + } + discoverytest.RunWithK8s(t, expectedLogs, []string{"splunk.discovery.receivers.prometheus/istio.enabled=true"}) +} + +func hasIstioProxyContainer(pod corev1.Pod) bool { + for _, container := range pod.Spec.Containers { + if container.Name == "istio-proxy" { + return true + } + } + return false +} +func runCommand(t *testing.T, command string) { + cmd := exec.Command("sh", "-c", command) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + require.NoError(t, cmd.Run(), "failed to run command: %s", command) +} + +func patchResource(t *testing.T, clientset *kubernetes.Clientset, namespace, name, resourceType, patch string) { + var err error + switch resourceType { + case "deployments": + _, err = clientset.AppsV1().Deployments(namespace).Patch(context.TODO(), name, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}) + require.NoError(t, err) + waitForDeploymentRollout(t, clientset, namespace, name) + case "services": + _, err = clientset.CoreV1().Services(namespace).Patch(context.TODO(), name, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}) + } + require.NoError(t, err) +} + +func waitForDeploymentRollout(t *testing.T, clientset *kubernetes.Clientset, namespace, name string) { + + err := wait.PollImmediate(5*time.Second, 5*time.Minute, func() (bool, error) { + deployment, err := clientset.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return false, err + } + if deployment.Status.UpdatedReplicas == *deployment.Spec.Replicas && + deployment.Status.Replicas == *deployment.Spec.Replicas && + deployment.Status.AvailableReplicas == *deployment.Spec.Replicas && + deployment.Status.ObservedGeneration >= deployment.Generation { + return true, nil + } + return false, nil + }) + require.NoError(t, err, "Deployment %s in namespace %s did not roll out successfully", name, namespace) +} + +func waitForPodsReady(t *testing.T, clientset *kubernetes.Clientset, namespace string) { + + pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) + require.NoError(t, err) + + for _, pod := range pods.Items { + if pod.DeletionTimestamp != nil { + continue + } + + err := wait.PollUntilContextTimeout(context.TODO(), 5*time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) { + p, err := clientset.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + for _, cond := range p.Status.Conditions { + if cond.Type == corev1.PodReady && cond.Status == corev1.ConditionTrue { + return true, nil + } + } + return false, nil + }) + if k8serrors.IsNotFound(err) { + continue + } + require.NoError(t, err, "Pod %s in namespace %s is not ready", pod.Name, pod.Namespace) + } +}