diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml index c5d66851a..2199f556b 100644 --- a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -58,9 +58,28 @@ spec: - mountPath: /root/.lnd/tls.cert name: config subPath: tls.cert + - name: shared-volume + mountPath: /root/.lnd/ {{- with .Values.extraContainers }} {{- toYaml . | nindent 4 }} {{- end }} + {{- if .Values.circuitbreaker.enabled }} + - name: circuitbreaker + image: {{ .Values.circuitbreaker.image | quote }} + imagePullPolicy: IfNotPresent + args: + - "--network={{ .Values.global.chain }}" + - "--rpcserver=localhost:{{ .Values.RPCPort }}" + - "--tlscertpath=/tls.cert" + - "--macaroonpath=/root/.lnd/data/chain/bitcoin/{{ .Values.global.chain }}/admin.macaroon" + - "--httplisten=0.0.0.0:{{ .Values.circuitbreaker.httpPort }}" + volumeMounts: + - name: shared-volume + mountPath: /root/.lnd/ + - name: config + mountPath: /tls.cert + subPath: tls.cert + {{- end }} volumes: {{- with .Values.volumes }} {{- toYaml . | nindent 4 }} @@ -68,6 +87,8 @@ spec: - configMap: name: {{ include "lnd.fullname" . }} name: config + - name: shared-volume + emptyDir: {} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/lnd/values.yaml b/resources/charts/bitcoincore/charts/lnd/values.yaml index d56e65bf4..971a8e72b 100644 --- a/resources/charts/bitcoincore/charts/lnd/values.yaml +++ b/resources/charts/bitcoincore/charts/lnd/values.yaml @@ -132,3 +132,8 @@ config: "" defaultConfig: "" channels: [] + +circuitbreaker: + enabled: false # Default to disabled + image: carlakirkcohen/circuitbreaker:attackathon-test + httpPort: 9235 \ No newline at end of file diff --git a/resources/networks/hello/network.yaml b/resources/networks/hello/network.yaml index f5acf0a83..75db08343 100644 --- a/resources/networks/hello/network.yaml +++ b/resources/networks/hello/network.yaml @@ -32,6 +32,9 @@ nodes: target: tank-0004-ln capacity: 100000 push_amt: 50000 + circuitbreaker: + enabled: true # This enables circuitbreaker for this node + httpPort: 9235 # Can override defaults per-node - name: tank-0004 addnode: @@ -85,3 +88,4 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post entrypoint: "../../plugins/hello" helloTo: "postNetwork!" podName: "hello-post-network" + \ No newline at end of file diff --git a/test/data/ln/network.yaml b/test/data/ln/network.yaml index 5d30686d4..8006b568f 100644 --- a/test/data/ln/network.yaml +++ b/test/data/ln/network.yaml @@ -21,6 +21,10 @@ nodes: target: tank-0004-ln capacity: 100000 push_amt: 50000 + circuitbreaker: + enabled: true + httpPort: 9235 + - name: tank-0004 addnode: - tank-0000 diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py index fdb479dbd..ea271abf4 100755 --- a/test/ln_basic_test.py +++ b/test/ln_basic_test.py @@ -2,9 +2,13 @@ import json import os +import random +import subprocess +import time from pathlib import Path from time import sleep +import requests from test_base import TestBase from warnet.process import stream_command @@ -24,11 +28,18 @@ def __init__(self): "tank-0005-ln", ] + self.cb_port = 9235 + self.cb_node = "tank-0003-ln" + self.port_forward = None + def run_test(self): try: # Wait for all nodes to wake up. ln_init will start automatically self.setup_network() + # Test circuit breaker API + self.test_circuit_breaker_api() + # Send a payment across channels opened automatically by ln_init self.pay_invoice(sender="tank-0005-ln", recipient="tank-0003-ln") @@ -39,6 +50,7 @@ def run_test(self): self.pay_invoice(sender="tank-0000-ln", recipient="tank-0002-ln") finally: + self.cleanup_kubectl_created_services() self.cleanup() def setup_network(self): @@ -120,6 +132,113 @@ def scenario_open_channels(self): self.log.info(f"Running scenario from: {scenario_file}") self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --debug") + def test_circuit_breaker_api(self): + self.log.info("Testing Circuit Breaker API") + + # Set up port forwarding to the circuit breaker + cb_url = self.setup_api_access(self.cb_node) + + self.log.info(f"Testing Circuit Breaker API at {cb_url}") + + # Test /info endpoint + info = self.cb_api_request(cb_url, "get", "/info") + assert "version" in info, "Circuit breaker info missing version" + + # Test /limits endpoint + limits = self.cb_api_request(cb_url, "get", "/limits") + assert isinstance(limits, dict), "Limits should be a dictionary" + + self.log.info("✅ Circuit Breaker API tests passed") + + def setup_api_access(self, pod_name): + """Set up Kubernetes Service access to the Circuit Breaker API""" + # Create a properly labeled service using kubectl expose + service_name = f"{pod_name}-svc" + + self.log.info(f"Creating service {service_name} for pod {pod_name}") + try: + subprocess.run( + [ + "kubectl", + "expose", + "pod", + pod_name, + "--name", + service_name, + "--port", + str(self.cb_port), + "--target-port", + str(self.cb_port), + ], + check=True, + ) + except subprocess.CalledProcessError as e: + self.log.error(f"Failed to create service: {e.stderr}") + raise + + time.sleep(51) # Wait for the service to be created + + service_url = f"http://{service_name}:{self.cb_port}/api" + self.service_to_cleanup = service_name + self.log.info(f"Service URL: {service_url}") + + self.log.info(f"Successfully created service at {service_url}") + return service_url + + def cb_api_request(self, base_url, method, endpoint, data=None): + """Universal API request handler with proper path handling""" + try: + # Parse the base URL components + url_parts = base_url.split("://")[1].split("/") + service_name = url_parts[0].split(":")[0] + port = url_parts[0].split(":")[1] if ":" in url_parts[0] else "80" + base_path = "/" + "/".join(url_parts[1:]) if len(url_parts) > 1 else "/" + + # Set up port forwarding + local_port = random.randint(10000, 20000) + pf = subprocess.Popen( + ["kubectl", "port-forward", f"svc/{service_name}", f"{local_port}:{port}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + try: + # Wait for port-forward to establish + time.sleep(2) + + # Construct the full local URL with proper path handling + full_path = base_path.rstrip("/") + "/" + endpoint.lstrip("/") + local_url = f"http://localhost:{local_port}{full_path}" + + self.log.debug(f"Attempting API request to: {local_url}") + + # Make the request + if method.lower() == "get": + response = requests.get(local_url, timeout=30) + else: + response = requests.post(local_url, json=data, timeout=30) + + response.raise_for_status() + return response.json() + + finally: + pf.terminate() + pf.wait() + + except Exception as e: + self.log.error(f"API request to {local_url} failed: {str(e)}") + raise + + def cleanup_kubectl_created_services(self): + """Clean up any created resources""" + if hasattr(self, "service_to_cleanup") and self.service_to_cleanup: + self.log.info(f"Deleting service {self.service_to_cleanup}") + subprocess.run( + ["kubectl", "delete", "svc", self.service_to_cleanup], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if __name__ == "__main__": test = LNBasicTest()