Skip to content

Commit 7f648db

Browse files
authored
Merge pull request #682 from bitcoin-dev-project/admin-scenarios
scenarios: add --admin flag to `run` command for ClusterRole access
2 parents e158f3f + 030eb8a commit 7f648db

File tree

14 files changed

+213
-4
lines changed

14 files changed

+213
-4
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
- simln_test.py
5353
- scenarios_test.py
5454
- namespace_admin_test.py
55+
- wargames_test.py
5556
steps:
5657
- uses: actions/checkout@v4
5758
- uses: azure/[email protected]

resources/charts/commander/templates/rbac.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,33 @@ subjects:
3333
- kind: ServiceAccount
3434
name: {{ include "commander.fullname" . }}
3535
namespace: {{ .Release.Namespace }}
36+
{{- if .Values.admin }}
37+
---
38+
apiVersion: rbac.authorization.k8s.io/v1
39+
kind: ClusterRole
40+
metadata:
41+
name: {{ include "commander.fullname" . }}
42+
namespace: {{ .Release.Namespace }}
43+
labels:
44+
app.kubernetes.io/name: {{ .Chart.Name }}
45+
rules:
46+
- apiGroups: [""]
47+
resources: ["pods", "namespaces", "configmaps"]
48+
verbs: ["get", "list", "watch"]
49+
---
50+
apiVersion: rbac.authorization.k8s.io/v1
51+
kind: ClusterRoleBinding
52+
metadata:
53+
name: {{ include "commander.fullname" . }}
54+
namespace: {{ .Release.Namespace }}
55+
labels:
56+
app.kubernetes.io/name: {{ .Chart.Name }}
57+
roleRef:
58+
kind: ClusterRole
59+
name: {{ include "commander.fullname" . }}
60+
apiGroup: rbac.authorization.k8s.io
61+
subjects:
62+
- kind: ServiceAccount
63+
name: {{ include "commander.fullname" . }}
64+
namespace: {{ .Release.Namespace }}
65+
{{- end}}

resources/charts/commander/values.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,5 @@ volumeMounts: []
6666
port:
6767

6868
args: ""
69+
70+
admin: false

resources/scenarios/commander.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,19 @@
2929
with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace") as f:
3030
NAMESPACE = f.read().strip()
3131

32-
# Use the in-cluster k8s client to determine what pods we have access to
32+
# Get the in-cluster k8s client to determine what we have access to
3333
config.load_incluster_config()
3434
sclient = client.CoreV1Api()
35-
pods = sclient.list_namespaced_pod(namespace=NAMESPACE)
36-
cmaps = sclient.list_namespaced_config_map(namespace=NAMESPACE)
35+
36+
try:
37+
# An admin with cluster access can list everything.
38+
# A wargames player with namespaced access will get a FORBIDDEN error here
39+
pods = sclient.list_pod_for_all_namespaces()
40+
cmaps = sclient.list_config_map_for_all_namespaces()
41+
except Exception:
42+
# Just get whatever we have access to in this namespace only
43+
pods = sclient.list_namespaced_pod(namespace=NAMESPACE)
44+
cmaps = sclient.list_namespaced_config_map(namespace=NAMESPACE)
3745

3846
WARNET = {"tanks": [], "lightning": [], "channels": []}
3947
for pod in pods.items:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
3+
# The base class exists inside the commander container
4+
try:
5+
from commander import Commander
6+
except Exception:
7+
from resources.scenarios.commander import Commander
8+
9+
10+
class GenOneAllNodes(Commander):
11+
def set_test_params(self):
12+
self.num_nodes = 1
13+
14+
def add_options(self, parser):
15+
parser.description = (
16+
"Attempt to generate one block on every node the scenario has access to"
17+
)
18+
parser.usage = "warnet run /path/to/generate_one_allnodes.py"
19+
20+
def run_test(self):
21+
for node in self.nodes:
22+
wallet = self.ensure_miner(node)
23+
addr = wallet.getnewaddress("bech32")
24+
self.log.info(f"node: {node.tank}")
25+
self.log.info(self.generatetoaddress(node, 1, addr))
26+
27+
28+
def main():
29+
GenOneAllNodes().main()
30+
31+
32+
if __name__ == "__main__":
33+
main()

src/warnet/control.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,26 +240,29 @@ def get_active_network(namespace):
240240
"--source_dir", type=click.Path(exists=True, file_okay=False, dir_okay=True), required=False
241241
)
242242
@click.argument("additional_args", nargs=-1, type=click.UNPROCESSED)
243+
@click.option("--admin", is_flag=True, default=False, show_default=False)
243244
@click.option("--namespace", default=None, show_default=True)
244245
def run(
245246
scenario_file: str,
246247
debug: bool,
247248
source_dir,
248249
additional_args: tuple[str],
250+
admin: bool,
249251
namespace: Optional[str],
250252
):
251253
"""
252254
Run a scenario from a file.
253255
Pass `-- --help` to get individual scenario help
254256
"""
255-
return _run(scenario_file, debug, source_dir, additional_args, namespace)
257+
return _run(scenario_file, debug, source_dir, additional_args, admin, namespace)
256258

257259

258260
def _run(
259261
scenario_file: str,
260262
debug: bool,
261263
source_dir,
262264
additional_args: tuple[str],
265+
admin: bool,
263266
namespace: Optional[str],
264267
) -> str:
265268
namespace = get_default_namespace_or(namespace)
@@ -329,6 +332,8 @@ def filter(path):
329332
]
330333

331334
# Add additional arguments
335+
if admin:
336+
helm_command.extend(["--set", "admin=true"])
332337
if additional_args:
333338
helm_command.extend(["--set", f"args={' '.join(additional_args)}"])
334339

@@ -347,6 +352,7 @@ def filter(path):
347352
except subprocess.CalledProcessError as e:
348353
print(f"Failed to deploy scenario commander: {scenario_name}")
349354
print(f"Error: {e.stderr}")
355+
return None
350356

351357
# upload scenario files and network data to the init container
352358
wait_for_init(name, namespace=namespace)

src/warnet/deploy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str
385385
debug=False,
386386
source_dir=SCENARIOS_DIR,
387387
additional_args=None,
388+
admin=False,
388389
namespace=namespace,
389390
)
390391
wait_for_pod(name, namespace=namespace)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
users:
2+
- name: warnet-user
3+
roles:
4+
- pod-viewer
5+
- pod-manager
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
namespaces:
2+
- name: wargames-red
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
nodes:
2+
- name: armada
3+
image:
4+
tag: '27.0'
5+
addnode:
6+
- miner.default
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
global:
2+
chain: regtest
3+
image:
4+
repository: bitcoindevproject/bitcoin
5+
pullPolicy: IfNotPresent
6+
tag: '27.0'
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
nodes:
2+
- name: miner
3+
image:
4+
tag: '27.0'
5+
- name: target-red
6+
addnode:
7+
- miner
8+
image:
9+
tag: '27.0'
10+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
global:
2+
chain: regtest
3+
image:
4+
repository: bitcoindevproject/bitcoin
5+
pullPolicy: IfNotPresent
6+
tag: '27.0'

test/wargames_test.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
from pathlib import Path
5+
6+
import pexpect
7+
from test_base import TestBase
8+
9+
from warnet.k8s import get_kubeconfig_value
10+
from warnet.process import stream_command
11+
12+
13+
class WargamesTest(TestBase):
14+
def __init__(self):
15+
super().__init__()
16+
self.wargame_dir = Path(os.path.dirname(__file__)) / "data" / "wargames"
17+
self.scen_src_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios"
18+
self.scen_test_dir = (
19+
Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" / "test_scenarios"
20+
)
21+
self.initial_context = get_kubeconfig_value("{.current-context}")
22+
23+
def run_test(self):
24+
try:
25+
self.setup_battlefield()
26+
self.setup_armies()
27+
self.check_scenario_permissions()
28+
finally:
29+
self.log.info("Restoring initial_context")
30+
stream_command(f"kubectl config use-context {self.initial_context}")
31+
self.cleanup()
32+
33+
def setup_battlefield(self):
34+
self.log.info("Setting up battlefield")
35+
self.log.info(self.warnet(f"deploy {self.wargame_dir / 'networks' / 'battlefield'}"))
36+
self.wait_for_all_tanks_status(target="running")
37+
self.wait_for_all_edges()
38+
39+
def setup_armies(self):
40+
self.log.info("Deploying namespaces and armadas")
41+
self.log.info(self.warnet(f"deploy {self.wargame_dir / 'namespaces' / 'armies'}"))
42+
self.log.info(
43+
self.warnet(f"deploy {self.wargame_dir / 'networks' / 'armada'} --to-all-users")
44+
)
45+
self.wait_for_all_tanks_status(target="running")
46+
self.wait_for_all_edges()
47+
48+
def check_scenario_permissions(self):
49+
self.log.info("Admin without --admin can not command a node outside of default namespace")
50+
stream_command(
51+
f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --debug"
52+
)
53+
# Only miner.default and target-red.default were accesible
54+
assert self.warnet("bitcoin rpc miner getblockcount") == "2"
55+
56+
self.log.info("Admin with --admin can command all nodes in any namespace")
57+
stream_command(
58+
f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --admin --debug"
59+
)
60+
# armada.wargames-red, miner.default and target-red.default were accesible
61+
assert self.warnet("bitcoin rpc miner getblockcount") == "5"
62+
63+
self.log.info("Switch to wargames player context")
64+
self.log.info(self.warnet("admin create-kubeconfigs"))
65+
clicker = pexpect.spawn("warnet auth kubeconfigs/warnet-user-wargames-red-kubeconfig")
66+
while clicker.expect(["Overwrite", "Updated kubeconfig"]) == 0:
67+
print(clicker.before, clicker.after)
68+
clicker.sendline("y")
69+
print(clicker.before, clicker.after)
70+
71+
self.log.info("Player without --admin can only command the node inside their own namespace")
72+
stream_command(
73+
f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --debug"
74+
)
75+
# Only armada.wargames-red was (and is) accesible
76+
assert self.warnet("bitcoin rpc armada getblockcount") == "6"
77+
78+
self.log.info("Player attempting to use --admin is gonna have a bad time")
79+
stream_command(
80+
f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --admin --debug"
81+
)
82+
# Nothing was accesible
83+
assert self.warnet("bitcoin rpc armada getblockcount") == "6"
84+
85+
self.log.info("Restore admin context")
86+
stream_command(f"kubectl config use-context {self.initial_context}")
87+
# Sanity check
88+
assert self.warnet("bitcoin rpc miner getblockcount") == "6"
89+
90+
91+
if __name__ == "__main__":
92+
test = WargamesTest()
93+
test.run_test()

0 commit comments

Comments
 (0)