Skip to content

Commit 533feb8

Browse files
authored
Merge pull request #22 from aojea/service_discovery
add e2e tests
2 parents a6b5975 + d367789 commit 533feb8

5 files changed

Lines changed: 242 additions & 16 deletions

File tree

cmd/sam-node/node.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
package main
1616

1717
import (
18+
"bufio"
1819
"context"
1920
"crypto/ed25519"
2021
"crypto/sha256"
2122
"encoding/hex"
2223
"fmt"
2324
"io"
24-
"bufio"
2525
"net/http"
2626
"net/http/httputil"
2727
"net/url"
@@ -110,13 +110,13 @@ func NewSamNode(ctx context.Context, privKey crypto.PrivKey, hubPubKey ed25519.P
110110
}
111111

112112
node := &SamNode{
113-
Store: store,
114-
trustedKeys: trustedKeys,
115-
knownPeers: make(map[string]bool),
116-
receivedMsgs: make(map[string][]string),
117-
topics: make(map[string]*pubsub.Topic),
118-
services: make(map[string]*ServiceManifest),
119-
LocalPolicy: localPolicy,
113+
Store: store,
114+
trustedKeys: trustedKeys,
115+
knownPeers: make(map[string]bool),
116+
receivedMsgs: make(map[string][]string),
117+
topics: make(map[string]*pubsub.Topic),
118+
services: make(map[string]*ServiceManifest),
119+
LocalPolicy: localPolicy,
120120
}
121121

122122
var err error
@@ -187,7 +187,7 @@ func NewSamNode(ctx context.Context, privKey crypto.PrivKey, hubPubKey ed25519.P
187187
node.DHT = kdht
188188

189189
if err := kdht.Bootstrap(ctx); err != nil {
190-
logger.Warnf("[DHT] Failed to bootstrap DHT: %v", err)
190+
return nil, fmt.Errorf("failed to bootstrap DHT: %w", err)
191191
}
192192

193193
// Bootstrap: Connect to the Hub
@@ -801,7 +801,7 @@ func (n *SamNode) RegisterService(ctx context.Context, req *api.RegisterServiceR
801801
}
802802

803803
if err := n.DHT.Provide(ctx, c, true); err != nil {
804-
return err
804+
return fmt.Errorf("failed to provide service to DHT: %w", err)
805805
}
806806

807807
var handler http.Handler
@@ -895,10 +895,10 @@ func (n *SamNode) DiscoverRemoteServices(ctx context.Context, serviceType api.Se
895895

896896
// Construct local proxy URL
897897
// Format: http://localhost:<sam_port>/sam/{peer_id}/{service_type}/{service_name}
898-
localURL := fmt.Sprintf("http://%s/sam/%s/%s/%s",
899-
n.BoundHTTPAddr,
900-
p.ID.String(),
901-
typeStr,
898+
localURL := fmt.Sprintf("http://%s/sam/%s/%s/%s",
899+
n.BoundHTTPAddr,
900+
p.ID.String(),
901+
typeStr,
902902
serviceName,
903903
)
904904

cmd/sam-node/sidecar.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ func handleRegisterService(node *SamNode, w http.ResponseWriter, r *http.Request
199199
}
200200

201201
if err := node.RegisterService(r.Context(), &req); err != nil {
202+
logger.Errorf("Failed to register service: %v", err)
202203
http.Error(w, fmt.Sprintf("Failed to register service: %v", err), http.StatusInternalServerError)
203204
return
204205
}

tests/e2e/datapath.bats

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
#!/usr/bin/env bats
2+
3+
load "lib/container_mesh.bash"
4+
5+
setup() {
6+
if ! mesh_require_docker; then
7+
skip "docker not available or daemon not running"
8+
fi
9+
10+
if [[ ! -x "./bin/sam-node" || ! -x "./bin/sam-hub" || ! -x "./bin/mcp-client" ]]; then
11+
skip "missing binaries; run: make build"
12+
fi
13+
14+
mesh_setup_env
15+
}
16+
17+
teardown() {
18+
mesh_cleanup_env
19+
# Cleanup any additional containers started in the test
20+
docker rm -f http-service sse-client >/dev/null 2>&1 || true
21+
}
22+
23+
@test "Datapath: HTTP and Stdio services are reachable across nodes" {
24+
run mesh_start_mock_oidc
25+
[[ "$status" -eq 0 ]]
26+
27+
# Start Hub
28+
mesh_start_hub
29+
local hub_name="${MESH_PREFIX}-hub"
30+
local hub_peer_id
31+
hub_peer_id=$(cat "/tmp/${MESH_PREFIX}-hub-peer-id")
32+
33+
# Start Node 1
34+
echo "[$(date +%T)] Starting Node 1"
35+
run mesh_start_node 1 "--discovery-interval 100ms --log-level debug"
36+
[[ "$status" -eq 0 ]]
37+
local node1_name="${MESH_PREFIX}-node-1"
38+
mesh_wait_for_log "${node1_name}" "SAM Node Online" 20
39+
mesh_wait_for_mcp_ready 1 20
40+
41+
local node1_peer_id
42+
node1_peer_id=$(docker logs "${node1_name}" 2>&1 | grep "PeerID:" | head -n 1 | awk '{print $2}' | tr -d '\r')
43+
44+
# Start Node 2
45+
echo "[$(date +%T)] Starting Node 2"
46+
run mesh_start_node 2 "--discovery-interval 100ms --log-level debug"
47+
[[ "$status" -eq 0 ]]
48+
local node2_name="${MESH_PREFIX}-node-2"
49+
mesh_wait_for_log "${node2_name}" "SAM Node Online" 20
50+
mesh_wait_for_mcp_ready 2 20
51+
52+
local node2_peer_id
53+
node2_peer_id=$(docker logs "${node2_name}" 2>&1 | grep "PeerID:" | head -n 1 | awk '{print $2}' | tr -d '\r')
54+
55+
# Explicitly connect Node 1 to Node 2 (DHT auto-discovery is slow/unreliable in this E2E setup)
56+
echo "[$(date +%T)] Explicitly connecting Node 1 to Node 2"
57+
local node2_addr="/dns4/sam-node-2/tcp/5002/p2p/${node2_peer_id}"
58+
run docker run --rm --network "${MESH_NETWORK}" -v "$(pwd)/bin/mcp-client:/mcp-client" python:3.12 /mcp-client -url "http://sam-node-1:8080/mcp/events" -tool "connect_peer" -args "{\"peer_addr\":\"${node2_addr}\"}"
59+
[[ "$status" -eq 0 ]]
60+
61+
# Verify connection
62+
mesh_wait_for_peer_connection 1 "${node2_peer_id}" 20
63+
[[ "$status" -eq 0 ]]
64+
65+
# 1. Setup HTTP Service on Node 1 side
66+
# Start a dummy HTTP server in a separate container
67+
echo "[$(date +%T)] Starting dummy HTTP service"
68+
docker run -d \
69+
--name http-service \
70+
--network "${MESH_NETWORK}" \
71+
python:3.12 python3 -c '
72+
from http.server import HTTPServer, BaseHTTPRequestHandler
73+
class S(BaseHTTPRequestHandler):
74+
def do_GET(self):
75+
self.send_response(200)
76+
self.send_header("Content-type", "application/json")
77+
self.end_headers()
78+
self.wfile.write(b"{\"status\":\"success\"}")
79+
HTTPServer(("0.0.0.0", 8000), S).serve_forever()
80+
'
81+
MESH_CONTAINERS+=("http-service")
82+
83+
# Register HTTP service on Node 1
84+
echo "[$(date +%T)] Registering HTTP service on Node 1"
85+
run docker run --rm --network "${MESH_NETWORK}" python:3.12 python3 -c "
86+
import urllib.request
87+
import json
88+
89+
data = {
90+
\"service\": {
91+
\"type\": 1,
92+
\"name\": \"http-tool\",
93+
\"description\": \"test http service\"
94+
},
95+
\"targetUrl\": \"http://http-service:8000\"
96+
}
97+
98+
req = urllib.request.Request(
99+
\"http://sam-node-1:8080/sam/service/register\",
100+
data=json.dumps(data).encode(\"utf-8\"),
101+
headers={
102+
\"Authorization\": \"Bearer secret-token\",
103+
\"Content-Type\": \"application/json\"
104+
}
105+
)
106+
with urllib.request.urlopen(req) as response:
107+
print(response.read().decode(\"utf-8\"))
108+
"
109+
echo "Register HTTP output: $output"
110+
[[ "$status" -eq 0 ]]
111+
[[ "$output" == *"Service registered"* ]]
112+
113+
# 2. Setup Stdio Service on Node 2 side
114+
# Register Stdio service (cat) on Node 2
115+
echo "[$(date +%T)] Registering Stdio service on Node 2"
116+
run docker run --rm --network "${MESH_NETWORK}" python:3.12 python3 -c "
117+
import urllib.request
118+
import json
119+
120+
data = {
121+
'service': {
122+
'type': 1,
123+
'name': 'stdio-tool',
124+
'description': 'test stdio service'
125+
},
126+
'command': {
127+
'command': ['sh', '-c', 'sleep 1; cat']
128+
}
129+
}
130+
131+
req = urllib.request.Request(
132+
'http://sam-node-2:8080/sam/service/register',
133+
data=json.dumps(data).encode('utf-8'),
134+
headers={
135+
'Authorization': 'Bearer secret-token',
136+
'Content-Type': 'application/json'
137+
}
138+
)
139+
with urllib.request.urlopen(req) as response:
140+
print(response.read().decode('utf-8'))
141+
"
142+
if [[ "$status" -ne 0 ]]; then
143+
echo "Node 2 logs:"
144+
docker logs "${node2_name}"
145+
fi
146+
[[ "$status" -eq 0 ]]
147+
[[ "$output" == *"Service registered"* ]]
148+
149+
# Wait for DHT propagation
150+
sleep 2
151+
152+
# 3. Test HTTP Datapath: Node 2 calls Node 1's HTTP service
153+
echo "[$(date +%T)] Testing HTTP Datapath from Node 2 to Node 1"
154+
run docker run --rm --network "${MESH_NETWORK}" python:3.12 python3 -c "
155+
import urllib.request
156+
req = urllib.request.Request(
157+
\"http://sam-node-2:8080/sam/${node1_peer_id}/mcp/http-tool/\",
158+
headers={\"Authorization\": \"Bearer secret-token\"}
159+
)
160+
with urllib.request.urlopen(req) as response:
161+
print(response.read().decode(\"utf-8\"))
162+
"
163+
echo "HTTP Call output: $output"
164+
[[ "$status" -eq 0 ]]
165+
[[ "$output" == *"{\"status\":\"success\"}"* ]]
166+
167+
# 4. Test Stdio Datapath: Node 1 calls Node 2's Stdio service
168+
echo "[$(date +%T)] Testing Stdio Datapath from Node 1 to Node 2"
169+
170+
# Start SSE client in background on Node 1 targeting Node 2's service
171+
docker run -d \
172+
--name sse-client \
173+
--network "${MESH_NETWORK}" \
174+
python:3.12 python3 -c "
175+
import urllib.request
176+
req = urllib.request.Request(
177+
\"http://sam-node-1:8080/sam/${node2_peer_id}/mcp/stdio-tool/\",
178+
headers={\"Authorization\": \"Bearer secret-token\"}
179+
)
180+
try:
181+
with urllib.request.urlopen(req) as response:
182+
for line in response:
183+
print(line.decode(\"utf-8\").strip(), flush=True)
184+
except Exception as e:
185+
print(f\"Error: {e}\", flush=True)
186+
"
187+
MESH_CONTAINERS+=("sse-client")
188+
189+
# Wait a bit for SSE stream to establish
190+
sleep 1
191+
192+
# Send message via POST from Node 1 to Node 2's service
193+
test_message="{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}"
194+
run docker run --rm --network "${MESH_NETWORK}" -e MSG="${test_message}" python:3.12 python3 -c "
195+
import urllib.request
196+
import os
197+
req = urllib.request.Request(
198+
\"http://sam-node-1:8080/sam/${node2_peer_id}/mcp/stdio-tool/\",
199+
data=os.environ['MSG'].encode('utf-8'),
200+
headers={
201+
\"Authorization\": \"Bearer secret-token\",
202+
\"Content-Type\": \"application/json\"
203+
}
204+
)
205+
with urllib.request.urlopen(req) as response:
206+
print(response.status)
207+
"
208+
echo "POST status: $output"
209+
[[ "$status" -eq 0 ]]
210+
[[ "$output" == *"200"* ]]
211+
212+
# Wait for message to echo back
213+
sleep 1
214+
215+
# Check SSE client logs for the echoed message
216+
run docker logs sse-client
217+
echo "SSE client logs: $output"
218+
[[ "$output" == *"data: ${test_message}"* ]]
219+
}

tests/e2e/docker/Dockerfile.sam-runtime

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
FROM alpine:3.20
22

3+
RUN apk add --no-cache busybox
4+
35
RUN addgroup -S sam && adduser -S -G sam sam
46

57
COPY bin/sam-node /usr/local/bin/sam-node

tests/e2e/lib/container_mesh.bash

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ if [[ -z "${MESH_HELPERS_LOADED:-}" ]]; then
4646

4747
mesh_setup_env() {
4848
mesh_cleanup_stale_resources
49+
50+
if ! docker image inspect "${MESH_RUNTIME_IMAGE}" >/dev/null 2>&1; then
51+
mesh_build_runtime_image
52+
fi
4953

5054
MESH_PREFIX="mesh-${BATS_TEST_NUMBER}-$$-$(date +%s)"
5155
MESH_NETWORK="${MESH_PREFIX}-net"
@@ -232,8 +236,8 @@ if [[ -z "${MESH_HELPERS_LOADED:-}" ]]; then
232236
--name "${name}" \
233237
--network "${MESH_NETWORK}" \
234238
--network-alias "sam-node-${idx}" \
235-
"sam-node:local" \
236-
run \
239+
"${MESH_RUNTIME_IMAGE}" \
240+
/usr/local/bin/sam-node run \
237241
${flags} \
238242
--hub "/dns4/sam-hub/tcp/4002/p2p/${hub_peer_id}" \
239243
--client-id "sam-e2e" \

0 commit comments

Comments
 (0)