Skip to content

Commit 635be37

Browse files
committed
Add circuit breaker plugin
1 parent 692230b commit 635be37

File tree

5 files changed

+140
-2
lines changed

5 files changed

+140
-2
lines changed

resources/charts/bitcoincore/charts/lnd/templates/pod.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,37 @@ spec:
5858
- mountPath: /root/.lnd/tls.cert
5959
name: config
6060
subPath: tls.cert
61+
- name: shared-volume
62+
mountPath: /root/.lnd/
6163
{{- with .Values.extraContainers }}
6264
{{- toYaml . | nindent 4 }}
6365
{{- end }}
66+
{{- if .Values.circuitbreaker.enabled }}
67+
- name: circuitbreaker
68+
image: {{ .Values.circuitbreaker.image | quote }}
69+
imagePullPolicy: IfNotPresent
70+
args:
71+
- "--network={{ .Values.global.chain }}"
72+
- "--rpcserver=localhost:{{ .Values.RPCPort }}"
73+
- "--tlscertpath=/tls.cert"
74+
- "--macaroonpath=/root/.lnd/data/chain/bitcoin/{{ .Values.global.chain }}/admin.macaroon"
75+
- "--httplisten=0.0.0.0:{{ .Values.circuitbreaker.httpPort }}"
76+
volumeMounts:
77+
- name: shared-volume
78+
mountPath: /root/.lnd/
79+
- name: config
80+
mountPath: /tls.cert
81+
subPath: tls.cert
82+
{{- end }}
6483
volumes:
6584
{{- with .Values.volumes }}
6685
{{- toYaml . | nindent 4 }}
6786
{{- end }}
6887
- configMap:
6988
name: {{ include "lnd.fullname" . }}
7089
name: config
90+
- name: shared-volume
91+
emptyDir: {}
7192
{{- with .Values.nodeSelector }}
7293
nodeSelector:
7394
{{- toYaml . | nindent 4 }}

resources/charts/bitcoincore/charts/lnd/values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,8 @@ config: ""
132132
defaultConfig: ""
133133

134134
channels: []
135+
136+
circuitbreaker:
137+
enabled: false # Default to disabled
138+
image: carlakirkcohen/circuitbreaker:attackathon-test
139+
httpPort: 9235

resources/networks/hello/network.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ nodes:
3232
target: tank-0004-ln
3333
capacity: 100000
3434
push_amt: 50000
35+
circuitbreaker:
36+
enabled: true # This enables circuitbreaker for this node
37+
httpPort: 9235 # Can override defaults per-node
3538

3639
- name: tank-0004
3740
addnode:
@@ -85,3 +88,4 @@ plugins: # Each plugin section has a number of hooks available (preDeploy, post
8588
entrypoint: "../../plugins/hello"
8689
helloTo: "postNetwork!"
8790
podName: "hello-post-network"
91+

test/data/ln/network.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ nodes:
2121
target: tank-0004-ln
2222
capacity: 100000
2323
push_amt: 50000
24+
circuitbreaker:
25+
enabled: true
26+
httpPort: 9235
27+
2428
- name: tank-0004
2529
addnode:
2630
- tank-0000

test/ln_basic_test.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
import json
44
import os
5+
import random
6+
import subprocess
7+
import time
58
from pathlib import Path
69
from time import sleep
710

11+
import requests
812
from test_base import TestBase
913

10-
from warnet.process import stream_command
14+
from warnet.process import run_command
1115

1216

1317
class LNBasicTest(TestBase):
@@ -24,11 +28,18 @@ def __init__(self):
2428
"tank-0005-ln",
2529
]
2630

31+
self.cb_port = 9235
32+
self.cb_node = "tank-0003-ln"
33+
self.port_forward = None
34+
2735
def run_test(self):
2836
try:
2937
# Wait for all nodes to wake up. ln_init will start automatically
3038
self.setup_network()
3139

40+
# Test circuit breaker API
41+
self.test_circuit_breaker_api()
42+
3243
# Send a payment across channels opened automatically by ln_init
3344
self.pay_invoice(sender="tank-0005-ln", recipient="tank-0003-ln")
3445

@@ -39,11 +50,12 @@ def run_test(self):
3950
self.pay_invoice(sender="tank-0000-ln", recipient="tank-0002-ln")
4051

4152
finally:
53+
self.cleanup_kubectl_created_services()
4254
self.cleanup()
4355

4456
def setup_network(self):
4557
self.log.info("Setting up network")
46-
stream_command(f"warnet deploy {self.network_dir}")
58+
run_command(f"warnet deploy {self.network_dir}")
4759

4860
def fund_wallets(self):
4961
outputs = ""
@@ -120,6 +132,98 @@ def scenario_open_channels(self):
120132
self.log.info(f"Running scenario from: {scenario_file}")
121133
self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --debug")
122134

135+
def test_circuit_breaker_api(self):
136+
self.log.info("Testing Circuit Breaker API")
137+
138+
# Set up port forwarding to the circuit breaker
139+
cb_url = self.setup_api_access(self.cb_node)
140+
141+
self.log.info(f"Testing Circuit Breaker API at {cb_url}")
142+
143+
# Test /info endpoint
144+
info = self.cb_api_request(cb_url, "get", "/info")
145+
assert "version" in info, "Circuit breaker info missing version"
146+
147+
# Test /limits endpoint
148+
limits = self.cb_api_request(cb_url, "get", "/limits")
149+
assert isinstance(limits, dict), "Limits should be a dictionary"
150+
151+
self.log.info("✅ Circuit Breaker API tests passed")
152+
153+
def setup_api_access(self, pod_name):
154+
"""Set up Kubernetes Service access to the Circuit Breaker API"""
155+
# Create a properly labeled service using kubectl expose
156+
service_name = f"{pod_name}-svc"
157+
158+
self.log.info(f"Creating service {service_name} for pod {pod_name}")
159+
160+
command = f"kubectl expose pod {pod_name} --name {service_name} --port {self.cb_port} --target-port {self.cb_port}"
161+
result = run_command(command)
162+
self.log.info(f"Service creation command output: {result}")
163+
164+
time.sleep(51) # Wait for the service to be created
165+
166+
service_url = f"http://{service_name}:{self.cb_port}/api"
167+
self.service_to_cleanup = service_name
168+
self.log.info(f"Service URL: {service_url}")
169+
170+
self.log.info(f"Successfully created service at {service_url}")
171+
return service_url
172+
173+
def cb_api_request(self, base_url, method, endpoint, data=None):
174+
"""Universal API request handler with proper path handling"""
175+
try:
176+
# Parse the base URL components
177+
url_parts = base_url.split("://")[1].split("/")
178+
service_name = url_parts[0].split(":")[0]
179+
port = url_parts[0].split(":")[1] if ":" in url_parts[0] else "80"
180+
base_path = "/" + "/".join(url_parts[1:]) if len(url_parts) > 1 else "/"
181+
182+
# Set up port forwarding
183+
local_port = random.randint(10000, 20000)
184+
pf = subprocess.Popen(
185+
["kubectl", "port-forward", f"svc/{service_name}", f"{local_port}:{port}"],
186+
stdout=subprocess.PIPE,
187+
stderr=subprocess.PIPE,
188+
)
189+
190+
try:
191+
# Wait for port-forward to establish
192+
time.sleep(2)
193+
194+
# Construct the full local URL with proper path handling
195+
full_path = base_path.rstrip("/") + "/" + endpoint.lstrip("/")
196+
local_url = f"http://localhost:{local_port}{full_path}"
197+
198+
self.log.debug(f"Attempting API request to: {local_url}")
199+
200+
# Make the request
201+
if method.lower() == "get":
202+
response = requests.get(local_url, timeout=30)
203+
else:
204+
response = requests.post(local_url, json=data, timeout=30)
205+
206+
response.raise_for_status()
207+
return response.json()
208+
209+
finally:
210+
pf.terminate()
211+
pf.wait()
212+
213+
except Exception as e:
214+
self.log.error(f"API request to {local_url} failed: {str(e)}")
215+
raise
216+
217+
def cleanup_kubectl_created_services(self):
218+
"""Clean up any created resources"""
219+
if hasattr(self, "service_to_cleanup") and self.service_to_cleanup:
220+
self.log.info(f"Deleting service {self.service_to_cleanup}")
221+
subprocess.run(
222+
["kubectl", "delete", "svc", self.service_to_cleanup],
223+
stdout=subprocess.DEVNULL,
224+
stderr=subprocess.DEVNULL,
225+
)
226+
123227

124228
if __name__ == "__main__":
125229
test = LNBasicTest()

0 commit comments

Comments
 (0)