Skip to content

Commit cde61e5

Browse files
Bre77claudejooola
authored
feat: parse nested load balancer label_selector targets (hetznercloud#633)
When a load balancer has a label_selector type target, the API returns a nested "targets" array containing the resolved individual server targets with their health statuses. This data was previously discarded. - Add `targets` parameter to `LoadBalancerTarget.__init__` in domain.py - Parse nested targets in `BoundLoadBalancer.__init__` for label_selector targets, creating `LoadBalancerTarget` objects with server, health_status, type, and use_private_ip fields - Add test fixture and test case for nested target parsing --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: jo <ljonas@riseup.net>
1 parent 7aaaa72 commit cde61e5

File tree

4 files changed

+90
-30
lines changed

4 files changed

+90
-30
lines changed

hcloud/load_balancers/client.py

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -93,36 +93,51 @@ def __init__(
9393
]
9494
data["private_net"] = private_nets
9595

96-
targets = data.get("targets")
97-
if targets:
98-
tmp_targets = []
99-
for target in targets:
100-
tmp_target = LoadBalancerTarget(type=target["type"])
101-
if target["type"] == "server":
102-
tmp_target.server = BoundServer(
103-
client._parent.servers, data=target["server"], complete=False
104-
)
105-
tmp_target.use_private_ip = target["use_private_ip"]
106-
elif target["type"] == "label_selector":
107-
tmp_target.label_selector = LoadBalancerTargetLabelSelector(
108-
selector=target["label_selector"]["selector"]
96+
def _load_balancer_targets(
97+
raw_targets: list[dict[str, Any]],
98+
) -> list[LoadBalancerTarget]:
99+
return [_load_balancer_target(raw_target) for raw_target in raw_targets]
100+
101+
def _load_balancer_target(
102+
raw_target: dict[str, Any],
103+
) -> LoadBalancerTarget:
104+
result = LoadBalancerTarget(type=raw_target["type"])
105+
106+
if raw_target["type"] == "ip":
107+
result.ip = LoadBalancerTargetIP(
108+
ip=raw_target["ip"]["ip"],
109+
)
110+
111+
elif raw_target["type"] == "server":
112+
result.server = BoundServer(
113+
client._parent.servers, # pylint: disable=protected-access
114+
data=raw_target["server"],
115+
complete=False,
116+
)
117+
result.use_private_ip = raw_target["use_private_ip"]
118+
119+
elif raw_target["type"] == "label_selector":
120+
result.label_selector = LoadBalancerTargetLabelSelector(
121+
selector=raw_target["label_selector"]["selector"]
122+
)
123+
result.use_private_ip = raw_target["use_private_ip"]
124+
125+
if (raw_nested_targets := raw_target.get("targets")) is not None:
126+
result.targets = _load_balancer_targets(raw_nested_targets)
127+
128+
if (raw_health_status := raw_target.get("health_status")) is not None:
129+
result.health_status = [
130+
LoadBalancerTargetHealthStatus(
131+
listen_port=item["listen_port"],
132+
status=item["status"],
109133
)
110-
tmp_target.use_private_ip = target["use_private_ip"]
111-
elif target["type"] == "ip":
112-
tmp_target.ip = LoadBalancerTargetIP(ip=target["ip"]["ip"])
113-
114-
target_health_status = target.get("health_status")
115-
if target_health_status is not None:
116-
tmp_target.health_status = [
117-
LoadBalancerTargetHealthStatus(
118-
listen_port=target_health_status_item["listen_port"],
119-
status=target_health_status_item["status"],
120-
)
121-
for target_health_status_item in target_health_status
122-
]
134+
for item in raw_health_status
135+
]
136+
137+
return result
123138

124-
tmp_targets.append(tmp_target)
125-
data["targets"] = tmp_targets
139+
if (raw_targets := data.get("targets")) is not None:
140+
data["targets"] = _load_balancer_targets(raw_targets)
126141

127142
services = data.get("services")
128143
if services:

hcloud/load_balancers/domain.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,8 @@ class LoadBalancerTarget(BaseDomain):
411411
use the private IP instead of primary public IP
412412
:param health_status: list
413413
List of health statuses of the services on this target. Only present for target types "server" and "ip".
414+
:param targets: list
415+
List of resolved label selector targets. Only present for target types "label_selector".
414416
"""
415417

416418
__api_properties__ = (
@@ -420,6 +422,7 @@ class LoadBalancerTarget(BaseDomain):
420422
"ip",
421423
"use_private_ip",
422424
"health_status",
425+
"targets",
423426
)
424427
__slots__ = __api_properties__
425428

@@ -431,13 +434,15 @@ def __init__(
431434
ip: LoadBalancerTargetIP | None = None,
432435
use_private_ip: bool | None = None,
433436
health_status: list[LoadBalancerTargetHealthStatus] | None = None,
437+
targets: list[LoadBalancerTarget] | None = None,
434438
):
435439
self.type = type
436440
self.server = server
437441
self.label_selector = label_selector
438442
self.ip = ip
439443
self.use_private_ip = use_private_ip
440444
self.health_status = health_status
445+
self.targets = targets
441446

442447
def to_payload(self) -> dict[str, Any]:
443448
"""

tests/unit/load_balancers/conftest.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,23 @@ def response_load_balancer():
8686
"health_status": [{"listen_port": 443, "status": "healthy"}],
8787
"label_selector": None,
8888
"use_private_ip": False,
89-
}
89+
},
90+
{
91+
"type": "label_selector",
92+
"label_selector": {"selector": "env=prod"},
93+
"use_private_ip": True,
94+
"targets": [
95+
{
96+
"type": "server",
97+
"server": {"id": 105054278},
98+
"use_private_ip": True,
99+
"health_status": [
100+
{"listen_port": 443, "status": "healthy"},
101+
{"listen_port": 3000, "status": "healthy"},
102+
],
103+
}
104+
],
105+
},
90106
],
91107
"algorithm": {"type": "round_robin"},
92108
}

tests/unit/load_balancers/test_client.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
)
2020
from hcloud.locations import Location
2121
from hcloud.networks import Network
22-
from hcloud.servers import Server
22+
from hcloud.servers import BoundServer, Server
2323

2424
from ..conftest import BoundModelTestCase
2525

@@ -60,6 +60,30 @@ def test_init(self, response_load_balancer):
6060
assert bound_load_balancer.id == 4711
6161
assert bound_load_balancer.name == "Web Frontend"
6262

63+
def test_init_label_selector_nested_targets(self, response_load_balancer):
64+
bound_load_balancer = BoundLoadBalancer(
65+
client=mock.MagicMock(), data=response_load_balancer["load_balancer"]
66+
)
67+
68+
label_selector_target = bound_load_balancer.targets[1]
69+
assert label_selector_target.type == "label_selector"
70+
assert label_selector_target.label_selector.selector == "env=prod"
71+
assert label_selector_target.use_private_ip is True
72+
assert label_selector_target.targets is not None
73+
assert len(label_selector_target.targets) == 1
74+
75+
nested = label_selector_target.targets[0]
76+
assert nested.type == "server"
77+
assert isinstance(nested.server, BoundServer)
78+
assert nested.server.id == 105054278
79+
assert nested.use_private_ip is True
80+
assert nested.health_status is not None
81+
assert len(nested.health_status) == 2
82+
assert nested.health_status[0].listen_port == 443
83+
assert nested.health_status[0].status == "healthy"
84+
assert nested.health_status[1].listen_port == 3000
85+
assert nested.health_status[1].status == "healthy"
86+
6387

6488
class TestLoadBalancerslient:
6589
@pytest.fixture()

0 commit comments

Comments
 (0)