Skip to content

Commit 65893a8

Browse files
authored
Add spec compliant GKE detection (#233)
1 parent 4ec3371 commit 65893a8

File tree

10 files changed

+283
-47
lines changed

10 files changed

+283
-47
lines changed

opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_detector.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from opentelemetry.resourcedetector.gcp_resource_detector import (
1818
_gce,
19+
_gke,
1920
_metadata,
2021
)
2122
from opentelemetry.sdk.resources import Resource, ResourceDetector
@@ -52,12 +53,31 @@ def detect(self) -> Resource:
5253
if not _metadata.is_available():
5354
return Resource.get_empty()
5455

56+
if _gke.on_gke():
57+
return _gke_resource()
5558
if _gce.on_gce():
5659
return _gce_resource()
5760

5861
return Resource.get_empty()
5962

6063

64+
def _gke_resource() -> Resource:
65+
zone_or_region = _gke.availability_zone_or_region()
66+
zone_or_region_key = (
67+
ResourceAttributes.CLOUD_AVAILABILITY_ZONE
68+
if zone_or_region.type == "zone"
69+
else ResourceAttributes.CLOUD_REGION
70+
)
71+
return _make_resource(
72+
{
73+
ResourceAttributes.CLOUD_PLATFORM_KEY: ResourceAttributes.GCP_KUBERNETES_ENGINE,
74+
zone_or_region_key: zone_or_region.value,
75+
ResourceAttributes.K8S_CLUSTER_NAME: _gke.cluster_name(),
76+
ResourceAttributes.HOST_ID: _gke.host_id(),
77+
}
78+
)
79+
80+
6181
def _gce_resource() -> Resource:
6282
zone_and_region = _gce.availability_zone_and_region()
6383
return _make_resource(

opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_gce.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616
import re
1717
from dataclasses import dataclass
1818

19-
from opentelemetry.resourcedetector.gcp_resource_detector._metadata import (
20-
MetadataAccessException,
21-
get_metadata,
22-
)
19+
from opentelemetry.resourcedetector.gcp_resource_detector import _metadata
2320

2421
# Format described in
2522
# https://cloud.google.com/compute/docs/metadata/default-metadata-values#vm_instance_metadata
@@ -32,8 +29,8 @@
3229

3330
def on_gce() -> bool:
3431
try:
35-
get_metadata()["instance"]["machineType"]
36-
except (MetadataAccessException, KeyError):
32+
_metadata.get_metadata()["instance"]["machineType"]
33+
except (_metadata.MetadataAccessException, KeyError):
3734
_logger.debug(
3835
"Could not fetch metadata attribute instance/machineType, "
3936
"assuming not on GCE.",
@@ -44,15 +41,15 @@ def on_gce() -> bool:
4441

4542

4643
def host_type() -> str:
47-
return get_metadata()["instance"]["machineType"]
44+
return _metadata.get_metadata()["instance"]["machineType"]
4845

4946

5047
def host_id() -> str:
51-
return get_metadata()["instance"]["id"]
48+
return _metadata.get_metadata()["instance"]["id"]
5249

5350

5451
def host_name() -> str:
55-
return get_metadata()["instance"]["name"]
52+
return _metadata.get_metadata()["instance"]["name"]
5653

5754

5855
@dataclass
@@ -62,7 +59,7 @@ class ZoneAndRegion:
6259

6360

6461
def availability_zone_and_region() -> ZoneAndRegion:
65-
full_zone = get_metadata()["instance"]["zone"]
62+
full_zone = _metadata.get_metadata()["instance"]["zone"]
6663
match = _ZONE_REGION_RE.search(full_zone)
6764
if not match:
6865
raise Exception(
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
from dataclasses import dataclass
17+
18+
from opentelemetry.resourcedetector.gcp_resource_detector import (
19+
_gce,
20+
_metadata,
21+
)
22+
23+
# TODO: remove when Python 3.7 is dropped
24+
from typing_extensions import Literal
25+
26+
KUBERNETES_SERVICE_HOST_ENV = "KUBERNETES_SERVICE_HOST"
27+
28+
29+
def on_gke() -> bool:
30+
return os.environ.get(KUBERNETES_SERVICE_HOST_ENV) is not None
31+
32+
33+
def host_id() -> str:
34+
return _gce.host_id()
35+
36+
37+
def cluster_name() -> str:
38+
return _metadata.get_metadata()["instance"]["attributes"]["cluster-name"]
39+
40+
41+
@dataclass
42+
class ZoneOrRegion:
43+
type: Literal["zone", "region"]
44+
value: str
45+
46+
47+
def availability_zone_or_region() -> ZoneOrRegion:
48+
cluster_location = _metadata.get_metadata()["instance"]["attributes"][
49+
"cluster-location"
50+
]
51+
hyphen_count = cluster_location.count("-")
52+
if hyphen_count == 1:
53+
return ZoneOrRegion(type="region", value=cluster_location)
54+
if hyphen_count == 2:
55+
return ZoneOrRegion(type="zone", value=cluster_location)
56+
raise Exception(
57+
f"unrecognized format for cluster location: {cluster_location}"
58+
)

opentelemetry-resourcedetector-gcp/src/opentelemetry/resourcedetector/gcp_resource_detector/_metadata.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ class Project(TypedDict):
3333
projectId: str
3434

3535

36+
Attributes = TypedDict(
37+
"Attributes", {"cluster-location": str, "cluster-name": str}
38+
)
39+
40+
3641
class Instance(TypedDict):
42+
attributes: Attributes
3743
id: str
3844
machineType: str
3945
name: str

opentelemetry-resourcedetector-gcp/tests/__snapshots__/test_detector.ambr

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,31 @@
2222
'telemetry.sdk.version': '1.15.0',
2323
})
2424
# ---
25+
# name: test_detects_gke[regional]
26+
dict({
27+
'cloud.account.id': 'fakeProject',
28+
'cloud.platform': 'gcp_kubernetes_engine',
29+
'cloud.provider': 'gcp',
30+
'cloud.region': 'us-east4',
31+
'host.id': 'fakeId',
32+
'k8s.cluster.name': 'fakeClusterName',
33+
'service.name': 'unknown_service',
34+
'telemetry.sdk.language': 'python',
35+
'telemetry.sdk.name': 'opentelemetry',
36+
'telemetry.sdk.version': '1.15.0',
37+
})
38+
# ---
39+
# name: test_detects_gke[zonal]
40+
dict({
41+
'cloud.account.id': 'fakeProject',
42+
'cloud.availability_zone': 'us-east4-b',
43+
'cloud.platform': 'gcp_kubernetes_engine',
44+
'cloud.provider': 'gcp',
45+
'host.id': 'fakeId',
46+
'k8s.cluster.name': 'fakeClusterName',
47+
'service.name': 'unknown_service',
48+
'telemetry.sdk.language': 'python',
49+
'telemetry.sdk.name': 'opentelemetry',
50+
'telemetry.sdk.version': '1.15.0',
51+
})
52+
# ---
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
from unittest.mock import MagicMock, patch
17+
18+
import pytest
19+
from opentelemetry.resourcedetector.gcp_resource_detector import _metadata
20+
21+
22+
@pytest.fixture(name="fake_get_metadata")
23+
def fixture_fake_get_metadata(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
24+
mock = MagicMock()
25+
monkeypatch.setattr(_metadata, "get_metadata", mock)
26+
return mock
27+
28+
29+
@pytest.fixture(name="fake_environ")
30+
def fixture_fake_environ():
31+
with patch.dict(os.environ, {}, clear=True):
32+
yield

opentelemetry-resourcedetector-gcp/tests/test_detector.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
1516
from unittest.mock import Mock
1617

1718
import pytest
@@ -22,27 +23,34 @@
2223
)
2324

2425

25-
@pytest.fixture(autouse=True)
26+
@pytest.fixture(name="reset_cache")
2627
def fixture_reset_cache():
2728
yield
2829
_metadata.get_metadata.cache_clear()
2930
_metadata.is_available.cache_clear()
3031

3132

32-
@pytest.fixture(name="fake_get", autouse=True)
33+
@pytest.fixture(name="fake_get")
3334
def fixture_fake_get(monkeypatch: pytest.MonkeyPatch):
3435
mock = Mock()
3536
monkeypatch.setattr(requests, "get", mock)
3637
return mock
3738

3839

39-
@pytest.fixture(name="fake_metadata", autouse=True)
40+
@pytest.fixture(name="fake_metadata")
4041
def fixture_fake_metadata(fake_get: Mock):
4142
json = {"instance": {}, "project": {}}
4243
fake_get().json.return_value = json
4344
return json
4445

4546

47+
# Reset stuff before every test
48+
# pylint: disable=unused-argument
49+
@pytest.fixture(autouse=True)
50+
def autouse(reset_cache, fake_get, fake_metadata, fake_environ):
51+
pass
52+
53+
4654
def test_detects_empty_when_not_available(snapshot, fake_get: Mock):
4755
fake_get.side_effect = requests.HTTPError()
4856
assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot
@@ -66,3 +74,35 @@ def test_detects_gce(snapshot, fake_metadata: _metadata.Metadata):
6674
)
6775

6876
assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot
77+
78+
79+
@pytest.mark.parametrize(
80+
"cluster_location",
81+
(
82+
pytest.param("us-east4", id="regional"),
83+
pytest.param("us-east4-b", id="zonal"),
84+
),
85+
)
86+
def test_detects_gke(
87+
cluster_location: str, snapshot, fake_metadata: _metadata.Metadata
88+
):
89+
os.environ["KUBERNETES_SERVICE_HOST"] = "fakehost"
90+
fake_metadata.update(
91+
# All the same attributes as GCE
92+
{
93+
"project": {"projectId": "fakeProject"},
94+
"instance": {
95+
"name": "fakeName",
96+
"id": "fakeId",
97+
"machineType": "fakeMachineType",
98+
"zone": "projects/233510669999/zones/us-east4-b",
99+
# Plus some attributes
100+
"attributes": {
101+
"cluster-name": "fakeClusterName",
102+
"cluster-location": cluster_location,
103+
},
104+
},
105+
}
106+
)
107+
108+
assert dict(GoogleCloudResourceDetector().detect().attributes) == snapshot

opentelemetry-resourcedetector-gcp/tests/test_gce.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,14 @@
2121
)
2222

2323

24-
@pytest.fixture(name="fake_get_metadata")
25-
def fixture_fake_get_metadata(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
26-
mock = MagicMock()
27-
monkeypatch.setattr(_gce, "get_metadata", mock)
28-
return mock
24+
# Reset stuff before every test
25+
# pylint: disable=unused-argument
26+
@pytest.fixture(autouse=True)
27+
def autouse(fake_get_metadata, fake_environ):
28+
pass
2929

3030

31-
# pylint: disable=unused-argument
32-
def test_detects_on_gce(fake_get_metadata: MagicMock) -> None:
31+
def test_detects_on_gce() -> None:
3332
assert _gce.on_gce()
3433

3534

0 commit comments

Comments
 (0)