diff --git a/CHANGELOG.md b/CHANGELOG.md index a48239ef1d..a80e49aa07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `opentelemetry-sdk-extension-aws`: Fix `AwsEksResourceDetector` on clusters using the EKS Access Entries API mode where the `aws-auth` ConfigMap is absent; `_is_eks` now decodes the pod service-account JWT `iss` claim instead of querying the Kubernetes API. + ([#4414](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4414)) - `opentelemetry-docker-tests`: Replace deprecated `SpanAttributes` from `opentelemetry.semconv.trace` with `opentelemetry.semconv._incubating.attributes` ([#4339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4339)) - `opentelemetry-instrumentation-confluent-kafka`: Skip `recv` span creation when `poll()` returns no message or `consume()` returns an empty list, avoiding empty spans on idle polls diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/eks.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/eks.py index 07f78b2d29..a15e5edcbd 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/eks.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/eks.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 import json import logging import os +import re import ssl from urllib.request import Request, urlopen @@ -32,6 +34,10 @@ _TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" _CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" +_EKS_OIDC_RE = re.compile( + r"^https://oidc\.eks\.[^.]+\.amazonaws\.com(?:\.cn)?/id/[A-F0-9]{32}$", + re.ASCII, +) def _aws_http_request(method, path, cred_value): @@ -60,11 +66,18 @@ def _get_k8s_cred_value(): def _is_eks(cred_value): - return _aws_http_request( - _GET_METHOD, - "/api/v1/namespaces/kube-system/configmaps/aws-auth", - cred_value, - ) + parts = cred_value.removeprefix("Bearer ").split(".") + if len(parts) != 3: + return False + try: + seg = parts[1] + payload = json.loads( + base64.urlsafe_b64decode(seg + "=" * (-len(seg) % 4)) + ) + except ValueError as exception: + logger.error("Failed to parse JWT for EKS detection: %s", exception) + return False + return bool(_EKS_OIDC_RE.match(payload.get("iss", ""))) def _get_cluster_info(cred_value): diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_eks.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_eks.py index a2adcb3969..236c82f780 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_eks.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_eks.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 +import json import unittest from collections import OrderedDict from unittest.mock import mock_open, patch @@ -25,6 +27,17 @@ ResourceAttributes, ) + +def _bearer_jwt(payload: dict) -> str: + header = base64.urlsafe_b64encode(b'{"alg":"RS256"}').rstrip(b"=").decode() + body = ( + base64.urlsafe_b64encode(json.dumps(payload).encode()) + .rstrip(b"=") + .decode() + ) + return f"Bearer {header}.{body}.fakesig" + + MockEksResourceAttributes = { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_EKS.value, @@ -138,3 +151,98 @@ def test_if_no_eks_paths_should_not_raise( AwsEksResourceDetector(raise_on_error=True).detect() except RuntimeError: self.fail("Should not raise") + + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", + return_value=_bearer_jwt( + { + "iss": "https://oidc.eks.eu-west-2.amazonaws.com/id/A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4" + } + ), + ) + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", + return_value=True, + ) + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._get_cluster_info", + return_value=f"""{{ + "data": {{ + "cluster.name": "{MockEksResourceAttributes[ResourceAttributes.K8S_CLUSTER_NAME]}" + }} +}} +""", + ) + @patch( + "builtins.open", + new_callable=mock_open, + read_data=f"14:name=systemd:/docker/{MockEksResourceAttributes[ResourceAttributes.CONTAINER_ID]}\n", + ) + def test_eks_oidc_jwt_detected( + self, + mock_open_function, + mock_get_cluster_info, + mock_is_k8s, + mock_get_k8s_cred_value, + ): + actual = AwsEksResourceDetector().detect() + self.assertEqual( + actual.attributes.get(ResourceAttributes.CLOUD_PLATFORM), + CloudPlatformValues.AWS_EKS.value, + ) + + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", + return_value=_bearer_jwt({"iss": "https://accounts.google.com"}), + ) + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", + return_value=True, + ) + def test_non_eks_jwt_returns_empty( + self, mock_is_k8s, mock_get_k8s_cred_value + ): + actual = AwsEksResourceDetector().detect() + self.assertEqual(actual.attributes, {}) + + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", + return_value=_bearer_jwt({"iss": "https://wrong.jwt.com"}), + ) + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", + return_value=True, + ) + def test_non_eks_jwt_should_raise( + self, mock_is_k8s, mock_get_k8s_cred_value + ): + with self.assertRaises(RuntimeError): + AwsEksResourceDetector(raise_on_error=True).detect() + + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", + return_value="Bearer notajwt.otel", + ) + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", + return_value=True, + ) + def test_is_eks_wrong_parts_count_should_raise( + self, mock_is_k8s, mock_get_k8s_cred_value + ): + with self.assertRaises(RuntimeError): + AwsEksResourceDetector(raise_on_error=True).detect() + + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._get_k8s_cred_value", + return_value="Bearer header.eyJpc3MiOg.fakesig", + ) + @patch( + "opentelemetry.sdk.extension.aws.resource.eks._is_k8s", + return_value=True, + ) + def test_is_eks_invalid_json_payload_should_raise( + self, mock_is_k8s, mock_get_k8s_cred_value + ): + with self.assertRaises(RuntimeError): + AwsEksResourceDetector(raise_on_error=True).detect()