Skip to content

Commit 746d397

Browse files
committed
update documentation and examples
This includes a *hello* network on the networks folder
1 parent bad42be commit 746d397

File tree

9 files changed

+165
-87
lines changed

9 files changed

+165
-87
lines changed

test/data/plugins/ln/network.yaml renamed to resources/networks/hello/network.yaml

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,36 +53,35 @@ nodes:
5353
ln:
5454
lnd: true
5555

56-
plugins:
57-
preDeploy:
56+
plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc)
57+
preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code
5858
hello:
59-
entrypoint: "../hello"
59+
entrypoint: "../../plugins/hello" # This entrypoint path is relative to the network.yaml file
6060
podName: "hello-pre-deploy"
6161
helloTo: "preDeploy!"
6262
postDeploy:
63-
simln:
64-
entrypoint: "../../../../resources/plugins/simln"
65-
activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]'
6663
hello:
67-
entrypoint: "../hello"
64+
entrypoint: "../../plugins/hello"
6865
podName: "hello-post-deploy"
6966
helloTo: "postDeploy!"
70-
preNode:
67+
simln: # You can have multiple plugins per hook
68+
entrypoint: "../../plugins/simln"
69+
activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]'
70+
preNode: # preNode plugins run before each node is deployed
7171
hello:
72-
entrypoint: "../hello"
72+
entrypoint: "../../plugins/hello"
7373
helloTo: "preNode!"
7474
postNode:
7575
hello:
76-
entrypoint: "../hello"
76+
entrypoint: "../../plugins/hello"
7777
helloTo: "postNode!"
7878
preNetwork:
7979
hello:
80-
entrypoint: "../hello"
80+
entrypoint: "../../plugins/hello"
8181
helloTo: "preNetwork!"
8282
podName: "hello-pre-network"
8383
postNetwork:
8484
hello:
85-
entrypoint: "../hello"
85+
entrypoint: "../../plugins/hello"
8686
helloTo: "postNetwork!"
8787
podName: "hello-post-network"
88-

resources/plugins/hello/README.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Hello Plugin
2+
3+
## Hello World!
4+
*Hello* is an example plugin to demonstrate the features of Warnet's plugin architecture.
5+
6+
## Usage
7+
In your python virtual environment with Warnet installed and started, create a new Warnet user folder (follow the prompts):
8+
9+
`$ warnet new user_folder`
10+
11+
`$ cd user_folder`
12+
13+
Deploy the *hello* network.
14+
15+
`$ warnet deploy networks/hello`
16+
17+
While that is launching, take a look inside the `networks/hello/network.yaml` file. You can also see the copy below which includes commentary on the structure of plugins in the `network.yaml` file.
18+
19+
Also, take a look at the `plugins/hello/plugin.py` file to see how plugins work and to find out how to author your own plugin.
20+
21+
To view the pods that the *hello* network launched, run `kubectl get all -A`
22+
23+
To view the various "Hello World!" messages, run `kubectl logs pod/POD_NAME`
24+
25+
### A `network.yaml` example
26+
When you initialize a new Warnet network, Warnet will create a new `network.yaml` file. You can modify these files to fit your needs.
27+
28+
For example, example `network.yaml` file includes the *hello* plugin, lightning nodes and the *simln* plugin.
29+
30+
<details>
31+
<summary>network.yaml</summary>
32+
33+
````yaml
34+
nodes:
35+
- name: tank-0000
36+
addnode:
37+
- tank-0001
38+
ln:
39+
lnd: true
40+
41+
- name: tank-0001
42+
addnode:
43+
- tank-0002
44+
ln:
45+
lnd: true
46+
47+
- name: tank-0002
48+
addnode:
49+
- tank-0000
50+
ln:
51+
lnd: true
52+
53+
- name: tank-0003
54+
addnode:
55+
- tank-0000
56+
ln:
57+
lnd: true
58+
lnd:
59+
config: |
60+
bitcoin.timelockdelta=33
61+
channels:
62+
- id:
63+
block: 300
64+
index: 1
65+
target: tank-0004-ln
66+
capacity: 100000
67+
push_amt: 50000
68+
69+
- name: tank-0004
70+
addnode:
71+
- tank-0000
72+
ln:
73+
lnd: true
74+
lnd:
75+
channels:
76+
- id:
77+
block: 300
78+
index: 2
79+
target: tank-0005-ln
80+
capacity: 50000
81+
push_amt: 25000
82+
83+
- name: tank-0005
84+
addnode:
85+
- tank-0000
86+
ln:
87+
lnd: true
88+
89+
plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc)
90+
preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code
91+
hello:
92+
entrypoint: "../../plugins/hello" # This entrypoint path is relative to the network.yaml file
93+
podName: "hello-pre-deploy"
94+
helloTo: "preDeploy!"
95+
postDeploy:
96+
hello:
97+
entrypoint: "../../plugins/hello"
98+
podName: "hello-post-deploy"
99+
helloTo: "postDeploy!"
100+
simln: # You can have multiple plugins per hook
101+
entrypoint: "../../plugins/simln"
102+
activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]'
103+
preNode: # preNode plugins run before each node is deployed
104+
hello:
105+
entrypoint: "../../plugins/hello"
106+
helloTo: "preNode!"
107+
postNode:
108+
hello:
109+
entrypoint: "../../plugins/hello"
110+
helloTo: "postNode!"
111+
preNetwork:
112+
hello:
113+
entrypoint: "../../plugins/hello"
114+
helloTo: "preNetwork!"
115+
podName: "hello-pre-network"
116+
postNetwork:
117+
hello:
118+
entrypoint: "../../plugins/hello"
119+
helloTo: "postNetwork!"
120+
podName: "hello-post-network"
121+
````
122+
123+
</details>
124+

test/data/plugins/hello/plugin.py renamed to resources/plugins/hello/plugin.py

Lines changed: 26 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
#!/usr/bin/env python3
22
import json
33
import logging
4+
from enum import Enum
45
from pathlib import Path
56
from typing import Optional
67

78
import click
8-
from kubernetes.stream import stream
99

1010
from warnet.constants import PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent
11-
from warnet.k8s import (
12-
get_default_namespace,
13-
get_static_client,
14-
)
1511
from warnet.process import run_command
1612

13+
# Tt is common for Warnet objects to have a "mission" tag to query them in the cluster.
1714
MISSION = "hello"
1815
PRIMARY_CONTAINER = MISSION
1916

@@ -35,6 +32,23 @@ class PluginError(Exception):
3532
log.propagate = True
3633

3734

35+
# Plugins look like this in the network.yaml file:
36+
#
37+
# plugins:
38+
# hello:
39+
# podName: "a-pod-name"
40+
# helloTo: "World!"
41+
#
42+
# "podName" and "helloTo" are essentially dictionary keys, and it helps to keep those keys in an
43+
# enum in order to prevent typos.
44+
class PluginContent(Enum):
45+
POD_NAME = ("podName",)
46+
HELLO_TO = "helloTo"
47+
48+
49+
# Warnet uses a python package called "click" to manage terminal interactions with the user.
50+
# To use click, we must declare a click "group" by decorating a function named after the plugin.
51+
# While optional, using click makes it easy for users to interact with your plugin.
3852
@click.group()
3953
@click.pass_context
4054
def hello(ctx):
@@ -44,6 +58,9 @@ def hello(ctx):
4458
ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir)
4559

4660

61+
# Each Warnet plugin must have an entrypoint function which takes two JSON objects: plugin_content
62+
# and warnet_content. We have seen the PluginContent enum above. Warnet also has a WarnetContent
63+
# enum which holds the keys to the warnet_content dictionary.
4764
@hello.command()
4865
@click.argument("plugin_content", type=str)
4966
@click.argument("warnet_content", type=str)
@@ -104,56 +121,13 @@ def get_data(plugin_content: dict) -> Optional[dict]:
104121
def _launch_pod(
105122
ctx, install_name: str = "hello", podName: str = "hello-pod", helloTo: str = "World!"
106123
):
107-
command = f"helm upgrade --install {install_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/hello --set podName={podName} --set helloTo={helloTo}"
124+
command = (
125+
f"helm upgrade --install {install_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/hello "
126+
f"--set podName={podName} --set helloTo={helloTo}"
127+
)
108128
log.info(command)
109129
log.info(run_command(command))
110130

111131

112-
def _sh(pod, method: str, params: tuple[str, ...]) -> str:
113-
namespace = get_default_namespace()
114-
115-
sclient = get_static_client()
116-
if params:
117-
cmd = [method]
118-
cmd.extend(params)
119-
else:
120-
cmd = [method]
121-
try:
122-
resp = stream(
123-
sclient.connect_get_namespaced_pod_exec,
124-
pod,
125-
namespace,
126-
container=PRIMARY_CONTAINER,
127-
command=cmd,
128-
stderr=True,
129-
stdin=False,
130-
stdout=True,
131-
tty=False,
132-
_preload_content=False,
133-
)
134-
stdout = ""
135-
stderr = ""
136-
while resp.is_open():
137-
resp.update(timeout=1)
138-
if resp.peek_stdout():
139-
stdout_chunk = resp.read_stdout()
140-
stdout += stdout_chunk
141-
if resp.peek_stderr():
142-
stderr_chunk = resp.read_stderr()
143-
stderr += stderr_chunk
144-
return stdout + stderr
145-
except Exception as err:
146-
print(f"Could not execute stream: {err}")
147-
148-
149-
@hello.command(context_settings={"ignore_unknown_options": True})
150-
@click.argument("pod", type=str)
151-
@click.argument("method", type=str)
152-
@click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments
153-
def sh(pod: str, method: str, params: tuple[str, ...]):
154-
"""Run shell commands in a pod"""
155-
print(_sh(pod, method, params))
156-
157-
158132
if __name__ == "__main__":
159133
hello()

resources/plugins/simln/plugin.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import click
88
from kubernetes.stream import stream
99

10-
# When we want to select pods based on their role in Warnet, we use "mission" tags. The "mission"
11-
# tag for "lightning" nodes is stored in LIGHTNING_MISSION.
1210
from warnet.constants import LIGHTNING_MISSION, PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent
1311
from warnet.k8s import (
1412
download,
@@ -20,10 +18,6 @@
2018
)
2119
from warnet.process import run_command
2220

23-
# Tt is common for Warnet objects to have a "mission" tag to query them in the cluster.
24-
# To make a "mission" tag for your plugin, declare it using the variable name MISSION. This will
25-
# be read by the warnet log system and status system.
26-
# This must match the pod's "mission" value in the plugin's associated helm file.
2721
MISSION = "simln"
2822
PRIMARY_CONTAINER = MISSION
2923

@@ -43,9 +37,6 @@ class PluginError(Exception):
4337
log.addHandler(console_handler)
4438

4539

46-
# Warnet uses a python package called "click" to manage terminal interactions with the user.
47-
# To use click, we must declare a click "group" by decorating a function named after the plugin.
48-
# Using click makes it easy for users to interact with your plugin.
4940
@click.group()
5041
@click.pass_context
5142
def simln(ctx):
@@ -55,10 +46,6 @@ def simln(ctx):
5546
ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir)
5647

5748

58-
# Each Warnet plugin must have an entrypoint function which takes a network_file_path and a
59-
# hook_value. Possible hook values can be found in the HookValue enum. It also takes a namespace
60-
# value and a variable number of arguments which is used by, for example, preNode and postNode to
61-
# pass along node names.
6249
@simln.command()
6350
@click.argument("plugin_content", type=str)
6451
@click.argument("warnet_content", type=str)
@@ -94,8 +81,6 @@ def _entrypoint(ctx, plugin_content: dict, warnet_content: dict):
9481
_launch_activity(activity, ctx.obj.get(PLUGIN_DIR_TAG))
9582

9683

97-
# The group name is then used in decorators to create commands. These commands are
98-
# available to users when they access your plugin from the command line.
9984
@simln.command()
10085
def list_pod_names():
10186
"""Get a list of SimLN pod names"""
@@ -110,10 +95,6 @@ def download_results(pod_name: str):
11095
print(f"Downloaded results to: {dest}")
11196

11297

113-
# When we want to use a command inside our plugin and also provide that command to the user, it
114-
# helps to create a private function whose name starts with an underscore. We also make a public
115-
# function with the same name except that we leave off the underscore, decorate it with the command
116-
# decorator, and also provide an instructive doc string for the user.
11798
def _get_example_activity() -> list[dict]:
11899
pods = get_mission(LIGHTNING_MISSION)
119100
try:
@@ -126,14 +107,12 @@ def _get_example_activity() -> list[dict]:
126107
return [{"source": pod_a, "destination": pod_b, "interval_secs": 1, "amount_msat": 2000}]
127108

128109

129-
# Notice how the command that we make available to the user simply calls our internal command.
130110
@simln.command()
131111
def get_example_activity():
132112
"""Get an activity representing node 2 sending msat to node 3"""
133113
print(json.dumps(_get_example_activity()))
134114

135115

136-
# Take note of how click expects us to explicitly declare command line arguments.
137116
@simln.command()
138117
@click.argument("activity", type=str)
139118
@click.pass_context

test/simln_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
class SimLNTest(TestBase):
1616
def __init__(self):
1717
super().__init__()
18-
self.network_dir = Path(os.path.dirname(__file__)) / "data" / "plugins" / "ln"
18+
self.network_dir = (
19+
Path(os.path.dirname(__file__)).parent / "resources" / "networks" / "hello"
20+
)
1921
self.plugins_dir = Path(os.path.dirname(__file__)).parent / "resources" / "plugins"
2022
self.simln_exec = "plugins/simln/plugin.py"
2123

0 commit comments

Comments
 (0)