diff --git a/client/gefyra/api/bridge.py b/client/gefyra/api/bridge.py index df4825c6..956a7324 100644 --- a/client/gefyra/api/bridge.py +++ b/client/gefyra/api/bridge.py @@ -3,13 +3,14 @@ from typing import List, Dict, TYPE_CHECKING from gefyra.exceptions import CommandTimeoutError, GefyraBridgeError +from kubernetes.client.exceptions import ApiException if TYPE_CHECKING: from gefyra.configuration import ClientConfiguration from gefyra.types import GefyraBridge -from .utils import stopwatch, wrap_bridge +from .utils import get_workload_type, stopwatch, wrap_bridge logger = logging.getLogger(__name__) @@ -37,12 +38,12 @@ def get_pods_to_intercept( def check_workloads( - pods_to_intercept, + pods_to_intercept: dict, workload_type: str, workload_name: str, container_name: str, namespace: str, - config, + config: "ClientConfiguration", ): from gefyra.cluster.resources import check_pod_valid_for_bridge @@ -57,11 +58,51 @@ def check_workloads( f"Could not find {workload_type}/{workload_name} to bridge. Available" f" {workload_type}: {', '.join(cleaned_names)}" ) + if container_name not in [ container for c_list in pods_to_intercept.values() for container in c_list ]: raise RuntimeError(f"Could not find container {container_name} to bridge.") + # Validate workload and probes + api = config.K8S_APP_API + core_api = config.K8S_CORE_API + try: + reconstructed_workload_type = get_workload_type(workload_type) + if reconstructed_workload_type == "pod": + workload = core_api.read_namespaced_pod(workload_name, namespace) + elif reconstructed_workload_type == "deployment": + workload = api.read_namespaced_deployment(workload_name, namespace) + elif reconstructed_workload_type == "statefulset": + workload = api.read_namespaced_stateful_set(workload_name, namespace) + except ApiException as e: + raise RuntimeError( + f"Error fetching workload {workload_type}/{workload_name}: {e}" + ) + + containers = ( + workload.spec.template.spec.containers + if hasattr(workload.spec, "template") + else workload.spec.containers + ) + target_container = next((c for c in containers if c.name == container_name), None) + if not target_container: + raise RuntimeError( + f"Container {container_name} not found in workload {workload_type}/{workload_name}." + ) + + def validate_http_probe(probe, probe_type): + if probe and probe.http_get is None: + raise RuntimeError( + f"{probe_type} in container {container_name} does not use httpGet. " + f"Only HTTP-based probes are supported." + ) + + # Check for HTTP probes only + validate_http_probe(target_container.liveness_probe, "LivenessProbe") + validate_http_probe(target_container.readiness_probe, "ReadinessProbe") + validate_http_probe(target_container.startup_probe, "StartupProbe") + for name in pod_names: check_pod_valid_for_bridge(config, name, namespace, container_name) diff --git a/client/tests/e2e/base.py b/client/tests/e2e/base.py index 6646320b..8fdf16a7 100644 --- a/client/tests/e2e/base.py +++ b/client/tests/e2e/base.py @@ -1112,6 +1112,36 @@ def test_t_install_presets(self): self.assert_namespace_not_found("gefyra") self.assert_cargo_not_running() + def test_u_unsupported_probes_throw_error(self): + res = self.gefyra_up() + self.assertTrue(res) + self.assert_gefyra_connected() + + # apply failing workload + self.provider.apply( + "../../operator/tests/fixtures/demo_pods_not_supported.yaml" + ) + + runner = CliRunner() + res = runner.invoke( + cli, + [ + "bridge", + "-N", + "test", + "-n", + "demo-failing", + "--target", + "deployment/frontend/frontend", + "--ports", + "80:8080", + "--connection-name", + CONNECTION_NAME, + ], + ) + + self.assert_gefyra_client_state("client-a", GefyraClientState.ERROR) + def test_util_for_connection_check(self): res = self.gefyra_up() self.assertTrue(res) diff --git a/operator/gefyra/bridge/carrier/__init__.py b/operator/gefyra/bridge/carrier/__init__.py index 7ae675e3..b032ee2f 100644 --- a/operator/gefyra/bridge/carrier/__init__.py +++ b/operator/gefyra/bridge/carrier/__init__.py @@ -1,5 +1,6 @@ import json from typing import Any, Dict, List, Optional +from gefyra.bridge.exceptions import BridgeInstallException from gefyra.utils import exec_command_pod import kubernetes as k8s @@ -139,11 +140,9 @@ def _patch_pod_with_carrier( self._get_all_probes(container), ) ): - self.logger.error( - "Not all of the probes to be handled are currently" - " supported by Gefyra" + raise BridgeInstallException( + message="Not all of the probes to be handled are currently supported by Gefyra" ) - return False, pod if ( container.image == f"{self.configuration.CARRIER_IMAGE}:{self.configuration.CARRIER_IMAGE_TAG}" @@ -157,8 +156,8 @@ def _patch_pod_with_carrier( container.image = f"{self.configuration.CARRIER_IMAGE}:{self.configuration.CARRIER_IMAGE_TAG}" break else: - raise RuntimeError( - f"Could not found container {self.container} in Pod {self.pod}" + raise BridgeInstallException( + message=f"Could not found container {self.container} in Pod {self.pod}" ) self.logger.info( f"Now patching Pod {self.pod}; container {self.container} with Carrier" diff --git a/operator/gefyra/bridge/exceptions.py b/operator/gefyra/bridge/exceptions.py new file mode 100644 index 00000000..677fb92b --- /dev/null +++ b/operator/gefyra/bridge/exceptions.py @@ -0,0 +1,5 @@ +from gefyra.exceptions import BridgeException + + +class BridgeInstallException(BridgeException): + pass diff --git a/operator/gefyra/bridgestate.py b/operator/gefyra/bridgestate.py index 83c7ea46..4eb88e37 100644 --- a/operator/gefyra/bridgestate.py +++ b/operator/gefyra/bridgestate.py @@ -7,10 +7,12 @@ import kubernetes as k8s from statemachine import State, StateMachine - from gefyra.base import GefyraStateObject, StateControllerMixin from gefyra.configuration import OperatorConfiguration +from gefyra.bridge.exceptions import BridgeInstallException +from gefyra.exceptions import BridgeException + class GefyraBridgeObject(GefyraStateObject): plural = "gefyrabridges" @@ -121,7 +123,11 @@ def _install_provider(self): It installs the bridge provider :return: Nothing """ - self.bridge_provider.install() + try: + self.bridge_provider.install() + except BridgeInstallException as be: + self.logger.debug(f"Encountered: {be}") + self.send("impair", exception=be) def _wait_for_provider(self): if not self.bridge_provider.ready(): @@ -189,3 +195,11 @@ def on_remove(self): def on_restore(self): self.bridge_provider.uninstall() self.send("terminate") + + def on_impair(self, exception: Optional[BridgeException] = None): + self.logger.error(f"Failed from {self.current_state}") + self.post_event( + reason=f"Failed from {self.current_state}", + message=exception.message, + _type="Warning", + ) diff --git a/operator/gefyra/exceptions.py b/operator/gefyra/exceptions.py new file mode 100644 index 00000000..1af400c1 --- /dev/null +++ b/operator/gefyra/exceptions.py @@ -0,0 +1,5 @@ +class BridgeException(Exception): + message: str + + def __init__(self, message: str): + self.message = message diff --git a/operator/poetry.lock b/operator/poetry.lock index fb621b84..88ed9354 100644 --- a/operator/poetry.lock +++ b/operator/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" diff --git a/operator/tests/e2e/test_create_bridge.py b/operator/tests/e2e/test_create_bridge.py index 2d7f1acc..acaf8886 100644 --- a/operator/tests/e2e/test_create_bridge.py +++ b/operator/tests/e2e/test_create_bridge.py @@ -132,3 +132,39 @@ def test_b_cleanup_bridges_routes( namespace="gefyra", timeout=60, ) + + +def test_c_fail_create_not_supported_bridges( + demo_backend_image, demo_frontend_image, carrier_image, operator: AClusterManager +): + k3d = operator + k3d.load_image(demo_backend_image) + k3d.load_image(demo_frontend_image) + k3d.load_image(carrier_image) + + k3d.kubectl(["create", "namespace", "demo-failing"]) + k3d.wait("ns/demo-failing", "jsonpath='{.status.phase}'=Active") + k3d.apply("tests/fixtures/demo_pods_not_supported.yaml") + k3d.wait( + "pod/frontend", + "condition=ready", + namespace="demo-failing", + timeout=60, + ) + + k3d.apply("tests/fixtures/a_gefyra_bridge_failing.yaml") + # bridge should be in error state + k3d.wait( + "gefyrabridges.gefyra.dev/bridge-a", + "jsonpath=.state=ERROR", + namespace="gefyra", + timeout=20, + ) + + # applying the bridge shouldn't have worked + k3d.wait( + "pod/frontend", + "condition=ready", + namespace="demo-failing", + timeout=60, + ) diff --git a/operator/tests/fixtures/a_gefyra_bridge_failing.yaml b/operator/tests/fixtures/a_gefyra_bridge_failing.yaml new file mode 100644 index 00000000..6c720e1f --- /dev/null +++ b/operator/tests/fixtures/a_gefyra_bridge_failing.yaml @@ -0,0 +1,14 @@ +apiVersion: gefyra.dev/v1 +kind: gefyrabridge +metadata: + name: bridge-a + namespace: gefyra +provider: carrier +connectionProvider: stowaway +client: client-a +targetNamespace: demo-failing +targetPod: frontend +targetContainer: frontend +portMappings: + - "8080:80" +destinationIP: "192.168.101.1" \ No newline at end of file diff --git a/operator/tests/fixtures/demo_pods_not_supported.yaml b/operator/tests/fixtures/demo_pods_not_supported.yaml new file mode 100644 index 00000000..faeece0c --- /dev/null +++ b/operator/tests/fixtures/demo_pods_not_supported.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: demo-failing +--- +apiVersion: v1 +kind: Pod +metadata: + name: frontend + namespace: demo-failing + labels: + app: frontend +spec: + containers: + - name: frontend + image: quay.io/gefyra/gefyra-demo-frontend + imagePullPolicy: IfNotPresent + ports: + - name: web + containerPort: 5003 + protocol: TCP + env: + - name: SVC_URL + value: "backend.demo.svc.cluster.local:5002" + livenessProbe: + exec: + command: + - ls + - /tmp + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - ls + - /tmp + initialDelaySeconds: 5 + periodSeconds: 5 \ No newline at end of file