Skip to content

Commit c575471

Browse files
authored
test: generate waku fleets config at runtime (#6902)
* feat: generate waku fleet config file at runtime * feat: expose PeerID from Messaging API * chore: cleanup * fix: paths for ci * fix: duplicate bootstrap nodes as static * fix: use container entrypoint * fix: only connect to the compose network * test: only connect to the docker compose network (#6901) * chore: remove duplicated waku DiscV5BootstrapNodes parameter * test: waku nodes discovery * chore: address comments * fix: rebase issues * test: increase discovery timeout to 30 seconds
1 parent 16fb384 commit c575471

File tree

12 files changed

+283
-34
lines changed

12 files changed

+283
-34
lines changed

Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ LABEL source="https://github.com/status-im/status-go"
4444
LABEL description="status-go is an underlying part of Status - a browser, messenger, and gateway to a decentralized world."
4545

4646
RUN apt-get update \
47-
&& apt-get install -y ca-certificates bash curl \
47+
&& apt-get install -y ca-certificates bash curl python3 \
4848
&& apt-get clean \
4949
&& rm -rf /var/lib/apt/lists/*
5050

@@ -54,10 +54,14 @@ RUN mkdir -p /static/configs
5454

5555
COPY --from=builder /go/src/github.com/status-im/status-go/build/bin/status-backend /usr/local/bin/
5656
COPY --from=builder /go/src/github.com/status-im/status-go/build/bin/push-notification-server /usr/local/bin/
57+
COPY --from=builder /go/src/github.com/status-im/status-go/tests-functional/scripts/scan_waku_fleet.py /usr/local/bin
5758
COPY --from=builder /go/src/github.com/status-im/status-go/static/keys/* /static/keys/
5859
COPY --from=builder /go/src/github.com/status-im/status-go/tests-functional/waku_configs/* /static/configs/
5960

61+
COPY _assets/scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
62+
RUN chmod +x /usr/local/bin/entrypoint.sh
63+
6064
# 30304 is used for Discovery v5
6165
EXPOSE 8080 8545 30303 30303/udp 30304/udp
62-
ENTRYPOINT ["status-backend"]
66+
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
6367
CMD ["--help"]

_assets/scripts/entrypoint.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
3+
# Define $SCAN_WAKU_FLEET to run a command before running the actual app (CMD).
4+
# This is expected to be used in functional tests to run `scan_waku_fleet.py` script before starting the app.
5+
$SCAN_WAKU_FLEET
6+
7+
# This will exec the CMD from your Dockerfile, i.e. "npm start"
8+
exec "$@"

messaging/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
ethcommon "github.com/ethereum/go-ethereum/common"
1616
"github.com/ethereum/go-ethereum/p2p/enode"
17+
1718
"github.com/status-im/status-go/connection"
1819
ethtypes "github.com/status-im/status-go/eth-node/types"
1920
"github.com/status-im/status-go/messaging/adapters"
@@ -225,6 +226,10 @@ func (a *API) Peers() types.PeerStats {
225226
return adapters.FromWakuPeerStats(a.core.transport.Peers())
226227
}
227228

229+
func (a *API) PeerID() peer.ID {
230+
return a.core.waku.PeerID()
231+
}
232+
228233
func (a *API) ConfirmMessagesProcessed(ids []string, timestamp uint64) error {
229234
return a.core.transport.ConfirmMessagesProcessed(ids, timestamp)
230235
}

messaging/waku/gowaku.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,6 @@ type Waku struct {
197197
// goingOnline is channel that notifies when connectivity has changed from offline to online
198198
goingOnline chan struct{}
199199

200-
// discV5BootstrapNodes is the ENR to be used to fetch bootstrap nodes for discovery
201-
discV5BootstrapNodes []string
202-
203200
onHistoricMessagesRequestFailed func([]byte, peer.AddrInfo, error)
204201
onPeerStats func(types.ConnStatus)
205202

@@ -266,7 +263,6 @@ func New(nodeKey *ecdsa.PrivateKey, cfg *Config, logger *zap.Logger, appDB *sql.
266263
timesource: ts,
267264
storeMsgIDsMu: sync.RWMutex{},
268265
logger: logger,
269-
discV5BootstrapNodes: cfg.DiscV5BootstrapNodes,
270266
onHistoricMessagesRequestFailed: onHistoricMessagesRequestFailed,
271267
onPeerStats: onPeerStats,
272268
onlineChecker: onlinechecker.NewDefaultOnlineChecker(false).(*onlinechecker.DefaultOnlineChecker),
@@ -308,7 +304,7 @@ func New(nodeKey *ecdsa.PrivateKey, cfg *Config, logger *zap.Logger, appDB *sql.
308304
}
309305

310306
if cfg.EnableDiscV5 {
311-
bootnodes, err := waku.getDiscV5BootstrapNodes(waku.ctx, cfg.DiscV5BootstrapNodes, false)
307+
bootnodes, err := waku.getDiscV5BootstrapNodes(waku.ctx, false)
312308
if err != nil {
313309
logger.Error("failed to get bootstrap nodes", zap.Error(err))
314310
return nil, err
@@ -397,7 +393,7 @@ func (w *Waku) GetNodeENRString() (string, error) {
397393
return w.node.ENR().String(), nil
398394
}
399395

400-
func (w *Waku) getDiscV5BootstrapNodes(ctx context.Context, addresses []string, useOnlyDnsDiscCache bool) ([]*enode.Node, error) {
396+
func (w *Waku) getDiscV5BootstrapNodes(ctx context.Context, useOnlyDnsDiscCache bool) ([]*enode.Node, error) {
401397
wg := sync.WaitGroup{}
402398
mu := sync.Mutex{}
403399
var result []*enode.Node
@@ -413,7 +409,7 @@ func (w *Waku) getDiscV5BootstrapNodes(ctx context.Context, addresses []string,
413409
}
414410
}
415411

416-
for _, addrString := range addresses {
412+
for _, addrString := range w.cfg.DiscV5BootstrapNodes {
417413
if addrString == "" {
418414
continue
419415
}
@@ -1867,7 +1863,7 @@ func (w *Waku) seedBootnodesForDiscV5() {
18671863
func (w *Waku) restartDiscV5(useOnlyDNSDiscCache bool) error {
18681864
ctx, cancel := context.WithTimeout(w.ctx, 30*time.Second)
18691865
defer cancel()
1870-
bootnodes, err := w.getDiscV5BootstrapNodes(ctx, w.discV5BootstrapNodes, useOnlyDNSDiscCache)
1866+
bootnodes, err := w.getDiscV5BootstrapNodes(ctx, useOnlyDNSDiscCache)
18711867
if err != nil {
18721868
return err
18731869
}

messaging/waku/waku_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func TestRestartDiscoveryV5(t *testing.T) {
104104

105105
require.Error(t, err)
106106

107-
w.discV5BootstrapNodes = []string{testStoreENRBootstrap}
107+
w.cfg.DiscV5BootstrapNodes = []string{testStoreENRBootstrap}
108108

109109
options = func(b *backoff.ExponentialBackOff) {
110110
b.MaxElapsedTime = 90 * time.Second

services/ext/api.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1627,3 +1627,7 @@ func (api *PublicAPI) GetCommunityMemberAllMessages(request *requests.CommunityM
16271627
func (api *PublicAPI) DeleteCommunityMemberMessages(request *requests.DeleteCommunityMemberMessages) (*protocol.MessengerResponse, error) {
16281628
return api.service.messenger.DeleteCommunityMemberMessages(request)
16291629
}
1630+
1631+
func (api *PublicAPI) PeerID() string {
1632+
return api.service.messaging.PeerID().String()
1633+
}

tests-functional/clients/services/wakuext.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,3 +449,8 @@ def get_activity_center_notifications(
449449

450450
response = self.rpc_request(method="activityCenterNotifications", params=[params])
451451
return response
452+
453+
def peer_id(self):
454+
params = []
455+
response = self.rpc_request("peerID", params)
456+
return response["result"]

tests-functional/clients/statusgo_container.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class StatusGoContainer:
2121
all_containers = []
2222
container = None
2323

24-
def __init__(self, entrypoint, ports=None, privileged=False, container_name_suffix=""):
24+
def __init__(self, cmd, ports=None, privileged=False, container_name_suffix=""):
2525
if ports is None:
2626
ports = {}
2727

@@ -53,9 +53,10 @@ def __init__(self, entrypoint, ports=None, privileged=False, container_name_suff
5353
"detach": True,
5454
"privileged": privileged,
5555
"name": self.container_name,
56-
"labels": {"com.docker.compose.project": docker_project_name}, # TODO: Is this still needed?
56+
"labels": {"com.docker.compose.project": docker_project_name},
5757
"environment": {
5858
"GOCOVERDIR": "/coverage/binary",
59+
"SCAN_WAKU_FLEET": self.get_waku_fleet_scan_command(),
5960
},
6061
"volumes": {
6162
coverage_path: {
@@ -66,9 +67,10 @@ def __init__(self, entrypoint, ports=None, privileged=False, container_name_suff
6667
"extra_hosts": {
6768
"host.docker.internal": "host-gateway",
6869
},
69-
"entrypoint": entrypoint,
70+
"command": cmd,
7071
"ports": ports,
7172
"stop_signal": "SIGINT",
73+
"network": self.network_name,
7274
}
7375

7476
if "FUNCTIONAL_TESTS_DOCKER_UID" in os.environ:
@@ -86,8 +88,23 @@ def __init__(self, entrypoint, ports=None, privileged=False, container_name_suff
8688

8789
logging.debug(f"Container {self.container.name} created. ID = {self.container.id}")
8890

89-
network = self.docker_client.networks.get(self.network_name)
90-
network.connect(self.container)
91+
def get_waku_fleet_scan_command(self):
92+
"""Returns the command string for scanning Waku fleet and generating config"""
93+
94+
# Known node names from docker compose
95+
bootstrap_nodes = "boot-1"
96+
static_nodes = "boot-1" # Add bootnode, otherwise metadata exchange doesn't happen, and Waku light mode doesn't work
97+
store_nodes = "store"
98+
99+
return (
100+
"python3 /usr/local/bin/scan_waku_fleet.py "
101+
f"--fleet-name {Config.waku_fleet} "
102+
f"--cluster-id 16 " # Cluster ID matches docker-compose.waku.yml
103+
f"--bootstrap-nodes {bootstrap_nodes} "
104+
f"--store-nodes {store_nodes} "
105+
f"--static-nodes {static_nodes} "
106+
f"--output {Config.waku_fleets_config}"
107+
)
91108

92109
def __del__(self):
93110
self.stop()

tests-functional/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def pytest_addoption(parser):
5757
"--waku-fleets-config",
5858
action="store",
5959
help="Path to a local JSON file with Waku fleets configuration. Default value is a path to config in Docker to run 2 local waku nodes",
60-
default="/static/configs/wakufleetconfig.json",
60+
default="/usr/status-user/wakufleetconfig.json",
6161
)
6262
parser.addoption(
6363
"--waku-fleet",
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import json
5+
import sys
6+
import os
7+
import logging
8+
9+
from urllib import request
10+
from typing import Dict, List, Optional, Tuple
11+
12+
13+
def print_config_options(args):
14+
logging.info("Configuration options:")
15+
logging.info(f" Fleet name: {args.fleet_name}")
16+
logging.info(f" Cluster ID: {args.cluster_id}")
17+
logging.info(f" Static nodes: {args.static_nodes}")
18+
logging.info(f" Bootstrap nodes: {args.bootstrap_nodes}")
19+
logging.info(f" Store nodes: {args.store_nodes}")
20+
logging.info(f" Output path: {args.output}")
21+
22+
23+
def parse_args():
24+
parser = argparse.ArgumentParser(description="Scan Waku nodes and build wakufleetconfig.json using DNS-based ENRs")
25+
parser.add_argument("--fleet-name", required=True, help="Fleet name, e.g. status-go.test")
26+
parser.add_argument("--cluster-id", required=True, type=int, help="Cluster ID, e.g. 16")
27+
parser.add_argument(
28+
"--static-nodes",
29+
default="",
30+
help="Comma-separated list of static node hostnames (Docker DNS names), e.g. 'node-1,node-2'",
31+
)
32+
parser.add_argument(
33+
"--bootstrap-nodes",
34+
default="",
35+
help="Comma-separated list of bootstrap node hostnames, e.g. 'boot-1,boot-2'",
36+
)
37+
parser.add_argument(
38+
"--store-nodes",
39+
default="",
40+
help="Comma-separated list of store node hostnames, e.g. 'store-1,store-2'",
41+
)
42+
parser.add_argument("--output", required=True, help="Output path for wakufleetconfig.json inside the container")
43+
return parser.parse_args()
44+
45+
46+
def _fetch_debug_info(host: str) -> Optional[Dict]:
47+
url = f"http://{host}:8645/debug/v1/info"
48+
logging.info(f"Fetching debug info from Waku node {url}")
49+
with request.urlopen(url, timeout=5.0) as resp:
50+
assert resp.status == 200
51+
data = resp.read()
52+
return json.loads(data.decode("utf-8"))
53+
54+
55+
def _first_dns_addr(listen_addrs: List[str]) -> Optional[str]:
56+
for addr in listen_addrs:
57+
if addr.startswith("/dns4/") or addr.startswith("/dns6/"):
58+
return addr
59+
return listen_addrs[0] if listen_addrs else None
60+
61+
62+
def _scan_hosts(hosts: List[str]) -> Dict[str, Tuple[str, Optional[str]]]:
63+
"""
64+
Returns mapping host -> (enrUri, addr)
65+
"""
66+
results: Dict[str, Tuple[str, Optional[str]]] = {}
67+
for host in hosts:
68+
host = host.strip()
69+
if not host:
70+
continue
71+
info = _fetch_debug_info(host)
72+
if not info:
73+
raise RuntimeError(f"Unable to gather ENR for host '{host}'")
74+
enr = info.get("enrUri")
75+
addrs = info.get("listenAddresses", []) or []
76+
addr = _first_dns_addr(addrs)
77+
if not enr:
78+
raise RuntimeError(f"No ENR in debug info for host '{host}'")
79+
results[host] = (enr, addr)
80+
logging.info(f"Host {host}: enr={enr}, addr={addr}")
81+
return results
82+
83+
84+
def main():
85+
logging.basicConfig(level=logging.INFO, format="[scan_waku_fleet] %(message)s")
86+
args = parse_args()
87+
print_config_options(args)
88+
89+
def split_list(s: str) -> List[str]:
90+
return [x.strip() for x in s.split(",") if x.strip()] if s else []
91+
92+
static_hosts = split_list(args.static_nodes)
93+
bootstrap_hosts = split_list(args.bootstrap_nodes)
94+
store_hosts = split_list(args.store_nodes)
95+
96+
all_hosts = list(dict.fromkeys(static_hosts + bootstrap_hosts + store_hosts)) # preserve order, unique
97+
98+
if not all_hosts:
99+
logging.info("No hosts provided. Nothing to do.")
100+
return 0
101+
102+
try:
103+
scanned = _scan_hosts(all_hosts)
104+
except Exception as e:
105+
logging.error(str(e))
106+
return 2
107+
108+
# Assemble the config structure
109+
fleet_name = args.fleet_name
110+
cluster_id = int(args.cluster_id)
111+
112+
# wakuNodes: use static hosts ENRs
113+
waku_nodes: List[str] = [scanned[h][0] for h in static_hosts if h in scanned]
114+
115+
# discV5BootstrapNodes: from bootstrap hosts ENRs
116+
bootstrap_enrs: List[str] = [scanned[h][0] for h in bootstrap_hosts if h in scanned]
117+
118+
# storeNodes: list of {id, enr, addr, fleet}
119+
store_nodes: List[Dict[str, str]] = []
120+
for h in store_hosts:
121+
if h not in scanned:
122+
continue
123+
enr, addr = scanned[h]
124+
node = {
125+
"id": h,
126+
"enr": enr,
127+
"fleet": fleet_name,
128+
}
129+
if addr:
130+
node["addr"] = addr
131+
store_nodes.append(node)
132+
133+
output = {
134+
fleet_name: {
135+
"clusterId": cluster_id,
136+
"wakuNodes": waku_nodes,
137+
"discV5BootstrapNodes": bootstrap_enrs,
138+
"storeNodes": store_nodes,
139+
}
140+
}
141+
142+
# Ensure destination directory exists
143+
os.makedirs(os.path.dirname(args.output or ".") or ".", exist_ok=True)
144+
145+
with open(args.output, "w", encoding="utf-8") as f:
146+
json.dump(output, f, indent=2)
147+
f.write("\n")
148+
149+
logging.info(f"Written fleet config for '{fleet_name}' to {args.output}")
150+
return 0
151+
152+
153+
if __name__ == "__main__":
154+
sys.exit(main())

0 commit comments

Comments
 (0)