Skip to content

Commit 6054467

Browse files
[DPE-5097] - test: add mtls int-tests (#133)
1 parent a0a8f0a commit 6054467

File tree

13 files changed

+582
-68
lines changed

13 files changed

+582
-68
lines changed

.github/workflows/sync_docs.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ jobs:
2626
- name: Show migrate output
2727
run: echo '${{ steps.docs-pr.outputs.migrate }}'
2828
- name: Show reconcile output
29-
run: echo '${{ steps.docs-pr.outputs.reconcile }}'
29+
run: echo '${{ steps.docs-pr.outputs.reconcile }}'

src/core/models.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -547,23 +547,23 @@ def pod(self) -> Pod:
547547
548548
K8s-only.
549549
"""
550-
return self.k8s.get_pod(pod_name=self.pod_name)
550+
return self.k8s.get_pod(self.pod_name)
551551

552552
@cached_property
553553
def node(self) -> Node:
554554
"""The Node the unit is scheduled on.
555555
556556
K8s-only.
557557
"""
558-
return self.k8s.get_node(pod=self.pod)
558+
return self.k8s.get_node(self.pod_name)
559559

560560
@cached_property
561561
def node_ip(self) -> str:
562562
"""The IPV4/IPV6 IP address the Node the unit is on.
563563
564564
K8s-only.
565565
"""
566-
return self.k8s.get_node_ip(node=self.node)
566+
return self.k8s.get_node_ip(self.pod_name)
567567

568568

569569
class ZooKeeper(RelationState):
@@ -700,7 +700,7 @@ def zookeeper_version(self) -> str:
700700
# retry to give ZK time to update its broker zNodes before failing
701701
@retry(
702702
wait=wait_fixed(5),
703-
stop=stop_after_attempt(10),
703+
stop=stop_after_attempt(3),
704704
retry=retry_if_result(lambda result: result is False),
705705
retry_error_callback=lambda _: False,
706706
)

src/events/broker.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,13 @@ def _on_start(self, event: StartEvent | PebbleReadyEvent) -> None:
163163
if not self.upgrade.idle:
164164
return
165165

166+
self.update_external_services()
167+
166168
self.charm._set_status(self.charm.state.ready_to_start)
167169
if not isinstance(self.charm.unit.status, ActiveStatus):
168170
event.defer()
169171
return
170172

171-
self.update_external_services()
172-
173173
# required settings given zookeeper connection config has been created
174174
self.config_manager.set_server_properties()
175175
self.config_manager.set_zk_jaas_config()

src/events/zookeeper.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,12 @@ def _on_zookeeper_changed(self, event: RelationChangedEvent) -> None:
7878

7979
try:
8080
internal_user_credentials = self._create_internal_credentials()
81-
except (KeyError, RuntimeError, subprocess.CalledProcessError, ExecError) as e:
82-
logger.warning(str(e))
81+
except (KeyError, RuntimeError) as e:
82+
logger.warning(e)
83+
event.defer()
84+
return
85+
except (subprocess.CalledProcessError, ExecError) as e:
86+
logger.warning(f"{e.stdout}, {e.stderr}")
8387
event.defer()
8488
return
8589

src/managers/k8s.py

+87-39
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
"""Manager for handling Kafka Kubernetes resources for a single Kafka pod."""
66

7+
import json
78
import logging
8-
from functools import cached_property
9+
import math
10+
import time
11+
from functools import cache
912

1013
from lightkube.core.client import Client
1114
from lightkube.core.exceptions import ApiError
@@ -15,13 +18,12 @@
1518

1619
from literals import SECURITY_PROTOCOL_PORTS, AuthMap, AuthMechanism
1720

18-
logger = logging.getLogger(__name__)
19-
2021
# default logging from lightkube httpx requests is very noisy
21-
logging.getLogger("lightkube").disabled = True
22-
logging.getLogger("lightkube.core.client").disabled = True
23-
logging.getLogger("httpx").disabled = True
24-
logging.getLogger("httpcore").disabled = True
22+
logging.getLogger("lightkube").setLevel(logging.CRITICAL)
23+
logging.getLogger("httpx").setLevel(logging.CRITICAL)
24+
logging.getLogger("httpcore").setLevel(logging.CRITICAL)
25+
26+
logger = logging.getLogger(__name__)
2527

2628

2729
class K8sManager:
@@ -42,54 +44,57 @@ def __init__(
4244
"SSL": "ssl",
4345
}
4446

45-
@cached_property
47+
def __eq__(self, other: object) -> bool:
48+
"""__eq__ dunder.
49+
50+
Needed to get an cache hit on calls on the same method from different instances of K8sManager
51+
as `self` is passed to methods.
52+
"""
53+
return isinstance(other, K8sManager) and self.__dict__ == other.__dict__
54+
55+
def __hash__(self) -> int:
56+
"""__hash__ dunder.
57+
58+
K8sManager needs to be hashable so that `self` can be passed to the 'dict-like' cache.
59+
"""
60+
return hash(json.dumps(self.__dict__, sort_keys=True))
61+
62+
@property
4663
def client(self) -> Client:
4764
"""The Lightkube client."""
4865
return Client( # pyright: ignore[reportArgumentType]
4966
field_manager=self.pod_name,
5067
namespace=self.namespace,
5168
)
5269

70+
@staticmethod
71+
def get_ttl_hash(seconds=60 * 2) -> int:
72+
"""Gets a unique time hash for the cache, expiring after 2 minutes.
73+
74+
When 2m has passed, a new value will be created, ensuring an cache miss
75+
and a re-loading of that K8s API call.
76+
"""
77+
return math.floor(time.time() / seconds)
78+
5379
# --- GETTERS ---
5480

5581
def get_pod(self, pod_name: str = "") -> Pod:
5682
"""Gets the Pod via the K8s API."""
57-
# Allows us to get pods from other peer units
58-
pod_name = pod_name or self.pod_name
59-
60-
return self.client.get(
61-
res=Pod,
62-
name=self.pod_name,
63-
)
83+
return self._get_pod(pod_name, self.get_ttl_hash())
6484

65-
def get_node(self, pod: Pod) -> Node:
85+
def get_node(self, pod_name: str) -> Node:
6686
"""Gets the Node the Pod is running on via the K8s API."""
67-
if not pod.spec or not pod.spec.nodeName:
68-
raise Exception("Could not find podSpec or nodeName")
69-
70-
return self.client.get(
71-
Node,
72-
name=pod.spec.nodeName,
73-
)
74-
75-
def get_node_ip(self, node: Node) -> str:
76-
"""Gets the IP Address of the Node via the K8s API."""
77-
# all these redundant checks are because Lightkube's typing is awful
78-
if not node.status or not node.status.addresses:
79-
raise Exception(f"No status found for {node}")
87+
return self._get_node(pod_name, self.get_ttl_hash())
8088

81-
for addresses in node.status.addresses:
82-
if addresses.type in ["ExternalIP", "InternalIP", "Hostname"]:
83-
return addresses.address
84-
85-
return ""
89+
def get_node_ip(self, pod_name: str) -> str:
90+
"""Gets the IP Address of the Node of a given Pod via the K8s API."""
91+
return self._get_node_ip(pod_name, self.get_ttl_hash())
8692

8793
def get_service(self, service_name: str) -> Service | None:
8894
"""Gets the Service via the K8s API."""
89-
return self.client.get(
90-
res=Service,
91-
name=service_name,
92-
)
95+
return self._get_service(service_name, self.get_ttl_hash())
96+
97+
# SERVICE BUILDERS
9398

9499
def get_node_port(
95100
self,
@@ -139,7 +144,7 @@ def get_bootstrap_nodeport(self, auth_map: AuthMap) -> int:
139144

140145
def build_bootstrap_services(self) -> Service:
141146
"""Builds a ClusterIP service for initial client connection."""
142-
pod = self.get_pod(pod_name=self.pod_name)
147+
pod = self.get_pod(self.pod_name)
143148
if not pod.metadata:
144149
raise Exception(f"Could not find metadata for {pod}")
145150

@@ -231,3 +236,46 @@ def apply_service(self, service: Service) -> None:
231236
return
232237
else:
233238
raise
239+
240+
# PRIVATE METHODS
241+
242+
@cache
243+
def _get_pod(self, pod_name: str = "", *_) -> Pod:
244+
# Allows us to get pods from other peer units
245+
pod_name = pod_name or self.pod_name
246+
247+
return self.client.get(
248+
res=Pod,
249+
name=pod_name,
250+
)
251+
252+
@cache
253+
def _get_node(self, pod_name: str, *_) -> Node:
254+
pod = self.get_pod(pod_name)
255+
if not pod.spec or not pod.spec.nodeName:
256+
raise Exception("Could not find podSpec or nodeName")
257+
258+
return self.client.get(
259+
Node,
260+
name=pod.spec.nodeName,
261+
)
262+
263+
@cache
264+
def _get_node_ip(self, pod_name: str, *_) -> str:
265+
# all these redundant checks are because Lightkube's typing is awful
266+
node = self.get_node(pod_name)
267+
if not node.status or not node.status.addresses:
268+
raise Exception(f"No status found for {node}")
269+
270+
for addresses in node.status.addresses:
271+
if addresses.type in ["ExternalIP", "InternalIP", "Hostname"]:
272+
return addresses.address
273+
274+
return ""
275+
276+
@cache
277+
def _get_service(self, service_name: str, *_) -> Service | None:
278+
return self.client.get(
279+
res=Service,
280+
name=service_name,
281+
)

tests/integration/app-charm/actions.yaml

+31
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,36 @@
33

44
produce:
55
description: Produces messages to a test-topic
6+
67
consume:
78
description: Consumes messages from a test-topic
9+
10+
create-certificate:
11+
description: Creates JKS keystore and signed certificate on unit
12+
13+
run-mtls-producer:
14+
description: Runs producer
15+
params:
16+
mtls-nodeport:
17+
type: integer
18+
description: The NodePort for the mTLS bootstrap service
19+
broker-ca:
20+
type: string
21+
description: The CA used for broker identity from certificates relation
22+
num-messages:
23+
type: integer
24+
description: The number of messages to be sent for testing
25+
26+
get-offsets:
27+
description: Retrieve offset for test topic
28+
params:
29+
mtls-nodeport:
30+
type: integer
31+
description: The NodePort for the mTLS bootstrap service
32+
33+
create-topic:
34+
description: Attempts the configured topic
35+
params:
36+
bootstrap-server:
37+
type: string
38+
description: The address for SASL_PLAINTEXT Kafka
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
options:
2+
topic-name:
3+
description: |
4+
The topic-name to request when relating to the Kafka application
5+
type: string
6+
default: test-topic

0 commit comments

Comments
 (0)