Skip to content

Commit c610996

Browse files
committed
feat(#739): add probe check in client and operator
- add e2e unit tests for operator
1 parent 2efb598 commit c610996

File tree

9 files changed

+229
-12
lines changed

9 files changed

+229
-12
lines changed

client/gefyra/api/bridge.py

+37-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import List, Dict, TYPE_CHECKING
44

55
from gefyra.exceptions import CommandTimeoutError, GefyraBridgeError
6+
from kubernetes.client.exceptions import ApiException
67

78
if TYPE_CHECKING:
89
from gefyra.configuration import ClientConfiguration
@@ -37,12 +38,12 @@ def get_pods_to_intercept(
3738

3839

3940
def check_workloads(
40-
pods_to_intercept,
41+
pods_to_intercept: dict,
4142
workload_type: str,
4243
workload_name: str,
4344
container_name: str,
4445
namespace: str,
45-
config,
46+
config: "ClientConfiguration",
4647
):
4748
from gefyra.cluster.resources import check_pod_valid_for_bridge
4849

@@ -57,11 +58,45 @@ def check_workloads(
5758
f"Could not find {workload_type}/{workload_name} to bridge. Available"
5859
f" {workload_type}: {', '.join(cleaned_names)}"
5960
)
61+
6062
if container_name not in [
6163
container for c_list in pods_to_intercept.values() for container in c_list
6264
]:
6365
raise RuntimeError(f"Could not find container {container_name} to bridge.")
6466

67+
# Validate workload and probes
68+
api = config.K8S_APP_API
69+
try:
70+
if workload_type == "deployment":
71+
workload = api.read_namespaced_deployment(workload_name, namespace)
72+
elif workload_type == "statefulset":
73+
workload = api.read_namespaced_stateful_set(workload_name, namespace)
74+
elif workload_type == "daemonset":
75+
workload = api.read_namespaced_daemon_set(workload_name, namespace)
76+
else:
77+
raise RuntimeError(f"Unsupported workload type: {workload_type}")
78+
except ApiException as e:
79+
raise RuntimeError(f"Error fetching workload {workload_type}/{workload_name}: {e}")
80+
81+
containers = workload.spec.template.spec.containers
82+
target_container = next(
83+
(c for c in containers if c.name == container_name), None
84+
)
85+
if not target_container:
86+
raise RuntimeError(f"Container {container_name} not found in workload {workload_type}/{workload_name}.")
87+
88+
def validate_http_probe(probe, probe_type):
89+
if probe and probe.http_get is None:
90+
raise RuntimeError(
91+
f"{probe_type} in container {container_name} does not use httpGet. "
92+
f"Only HTTP-based probes are supported."
93+
)
94+
95+
# Check for HTTP probes only
96+
validate_http_probe(target_container.liveness_probe, "LivenessProbe")
97+
validate_http_probe(target_container.readiness_probe, "ReadinessProbe")
98+
validate_http_probe(target_container.startup_probe, "StartupProbe")
99+
65100
for name in pod_names:
66101
check_pod_valid_for_bridge(config, name, namespace, container_name)
67102

operator/gefyra/bridge/carrier/__init__.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
from typing import Any, Dict, List, Optional
3-
from gefyra.utils import exec_command_pod
3+
from gefyra.utils import BridgeException, exec_command_pod
44
import kubernetes as k8s
55

66
from gefyra.bridge.abstract import AbstractGefyraBridgeProvider
@@ -33,7 +33,10 @@ def __init__(
3333

3434
def install(self, parameters: Optional[Dict[Any, Any]] = None):
3535
parameters = parameters or {}
36-
self._patch_pod_with_carrier(handle_probes=parameters.get("handleProbes", True))
36+
try:
37+
self._patch_pod_with_carrier(handle_probes=parameters.get("handleProbes", True))
38+
except BridgeException as be:
39+
raise BridgeException from be
3740

3841
def _ensure_probes(self, container: k8s.client.V1Container) -> bool:
3942
probes = self._get_all_probes(container)
@@ -143,7 +146,7 @@ def _patch_pod_with_carrier(
143146
"Not all of the probes to be handled are currently"
144147
" supported by Gefyra"
145148
)
146-
return False, pod
149+
raise BridgeException()
147150
if (
148151
container.image
149152
== f"{self.configuration.CARRIER_IMAGE}:{self.configuration.CARRIER_IMAGE_TAG}"

operator/gefyra/bridgestate.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime
1+
from datetime import datetime, UTC
22
from typing import Any, Optional
33
from gefyra.bridge.abstract import AbstractGefyraBridgeProvider
44
from gefyra.bridge.factory import BridgeProviderType, bridge_provider_factory
@@ -7,10 +7,11 @@
77
import kubernetes as k8s
88
from statemachine import State, StateMachine
99

10-
1110
from gefyra.base import GefyraStateObject, StateControllerMixin
1211
from gefyra.configuration import OperatorConfiguration
1312

13+
from gefyra.utils import BridgeException
14+
1415

1516
class GefyraBridgeObject(GefyraStateObject):
1617
plural = "gefyrabridges"
@@ -37,7 +38,7 @@ class GefyraBridge(StateMachine, StateControllerMixin):
3738

3839
install = (
3940
requested.to(installing, on="_install_provider")
40-
| error.to(installing)
41+
| installing.to(error)
4142
| installing.to.itself(on="_wait_for_provider")
4243
)
4344
set_installed = (
@@ -106,7 +107,7 @@ def sunset(self) -> Optional[datetime]:
106107

107108
@property
108109
def should_terminate(self) -> bool:
109-
if self.sunset and self.sunset <= datetime.utcnow():
110+
if self.sunset and self.sunset <= datetime.now(UTC):
110111
# remove this bridge because the sunset time is in the past
111112
self.logger.warning(
112113
f"Bridge '{self.object_name}' should be terminated "
@@ -121,7 +122,10 @@ def _install_provider(self):
121122
It installs the bridge provider
122123
:return: Nothing
123124
"""
124-
self.bridge_provider.install()
125+
try:
126+
self.bridge_provider.install()
127+
except BridgeException:
128+
self.send("impair")
125129

126130
def _wait_for_provider(self):
127131
if not self.bridge_provider.ready():

operator/gefyra/utils.py

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
logger = logging.getLogger("gefyra.utils")
1414

1515

16+
class BridgeException(Exception):
17+
pass
18+
19+
1620
def get_label_selector(labels: dict[str, str]) -> str:
1721
return ",".join(["{0}={1}".format(*label) for label in list(labels.items())])
1822

operator/poetry.lock

+34-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

operator/pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ kopf = "^1.37.3"
1010
kubernetes = "^31.0.0"
1111
python-decouple = "^3.8"
1212
python-statemachine = "^2.4.0"
13+
pydot = "^3.0.3"
1314

1415
[tool.poetry.group.dev.dependencies]
1516
pytest = "^7.4"

operator/tests/e2e/test_create_bridge.py

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import json
22
import logging
3+
import subprocess
4+
import pytest
35
from pytest_kubernetes.providers import AClusterManager
46
from .utils import GefyraDockerClient
57

@@ -101,7 +103,6 @@ def test_a_bridge(
101103

102104
gclient_a.delete()
103105

104-
105106
def test_b_cleanup_bridges_routes(
106107
carrier_image,
107108
operator: AClusterManager,
@@ -132,3 +133,44 @@ def test_b_cleanup_bridges_routes(
132133
namespace="gefyra",
133134
timeout=60,
134135
)
136+
137+
def test_c_fail_create_not_supported_bridges(
138+
demo_backend_image,
139+
demo_frontend_image,
140+
carrier_image,
141+
operator: AClusterManager
142+
):
143+
k3d = operator
144+
k3d.load_image(demo_backend_image)
145+
k3d.load_image(demo_frontend_image)
146+
k3d.load_image(carrier_image)
147+
148+
k3d.kubectl(["create", "namespace", "demo-failing"])
149+
k3d.wait("ns/demo-failing", "jsonpath='{.status.phase}'=Active")
150+
k3d.apply("tests/fixtures/demo_pods_not_supported.yaml")
151+
k3d.wait(
152+
"pod/backend",
153+
"condition=ready",
154+
namespace="demo-failing",
155+
timeout=60,
156+
)
157+
158+
k3d.apply("tests/fixtures/a_gefyra_bridge_failing.yaml")
159+
# bridge should be in error state
160+
k3d.wait(
161+
"gefyrabridges.gefyra.dev/bridge-a",
162+
"jsonpath=.state=ERROR",
163+
namespace="gefyra",
164+
timeout=20,
165+
)
166+
167+
# applying the bridge shouldn't have worked
168+
with pytest.raises(subprocess.TimeoutExpired):
169+
k3d.wait(
170+
"pod/frontend",
171+
"jsonpath=.status.containerStatuses[0].image=docker.io/library/"
172+
+ carrier_image,
173+
namespace="demo-failing",
174+
timeout=60,
175+
)
176+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: gefyra.dev/v1
2+
kind: gefyrabridge
3+
metadata:
4+
name: bridge-a
5+
namespace: gefyra
6+
provider: carrier
7+
connectionProvider: stowaway
8+
client: client-a
9+
targetNamespace: demo-failing
10+
targetPod: frontend
11+
targetContainer: frontend
12+
portMappings:
13+
- "8080:80"
14+
destinationIP: "192.168.101.1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
apiVersion: v1
2+
kind: Pod
3+
metadata:
4+
name: backend
5+
namespace: demo-failing
6+
labels:
7+
app: backend
8+
spec:
9+
securityContext:
10+
runAsUser: 1000
11+
runAsGroup: 1000
12+
fsGroup: 1000
13+
containers:
14+
- name: backend
15+
image: quay.io/gefyra/gefyra-demo-backend
16+
imagePullPolicy: IfNotPresent
17+
ports:
18+
- name: web
19+
containerPort: 5002
20+
protocol: TCP
21+
---
22+
apiVersion: v1
23+
kind: Pod
24+
metadata:
25+
name: frontend
26+
namespace: demo-failing
27+
labels:
28+
app: frontend
29+
spec:
30+
containers:
31+
- name: frontend
32+
image: quay.io/gefyra/gefyra-demo-frontend
33+
imagePullPolicy: IfNotPresent
34+
ports:
35+
- name: web
36+
containerPort: 5003
37+
protocol: TCP
38+
env:
39+
- name: SVC_URL
40+
value: "backend.demo.svc.cluster.local:5002"
41+
livenessProbe:
42+
exec:
43+
command:
44+
- cat
45+
- /tmp/healthy
46+
initialDelaySeconds: 30
47+
periodSeconds: 10
48+
readinessProbe:
49+
exec:
50+
command:
51+
- cat
52+
- /tmp/ready
53+
initialDelaySeconds: 5
54+
periodSeconds: 5
55+
---
56+
apiVersion: v1
57+
kind: Service
58+
metadata:
59+
name: backend
60+
namespace: demo-failing
61+
spec:
62+
selector:
63+
app: backend
64+
ports:
65+
- protocol: TCP
66+
port: 5002
67+
targetPort: 5002
68+
---
69+
apiVersion: v1
70+
kind: Service
71+
metadata:
72+
name: frontend
73+
namespace: demo-failing
74+
spec:
75+
selector:
76+
app: frontend
77+
ports:
78+
- protocol: "TCP"
79+
port: 80
80+
targetPort: 5003
81+
type: LoadBalancer

0 commit comments

Comments
 (0)