Skip to content

Commit

Permalink
update documentation and examples
Browse files Browse the repository at this point in the history
This includes a *hello* network on the networks folder
  • Loading branch information
mplsgrant committed Dec 13, 2024
1 parent aeeba89 commit ce1efd6
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,36 +53,35 @@ nodes:
ln:
lnd: true

plugins:
preDeploy:
plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc)
preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code
hello:
entrypoint: "../hello"
entrypoint: "../../plugins/hello" # This entrypoint path is relative to the network.yaml file
podName: "hello-pre-deploy"
helloTo: "preDeploy!"
postDeploy:
simln:
entrypoint: "../../../../resources/plugins/simln"
activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]'
hello:
entrypoint: "../hello"
entrypoint: "../../plugins/hello"
podName: "hello-post-deploy"
helloTo: "postDeploy!"
preNode:
simln: # You can have multiple plugins per hook
entrypoint: "../../plugins/simln"
activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]'
preNode: # preNode plugins run before each node is deployed
hello:
entrypoint: "../hello"
entrypoint: "../../plugins/hello"
helloTo: "preNode!"
postNode:
hello:
entrypoint: "../hello"
entrypoint: "../../plugins/hello"
helloTo: "postNode!"
preNetwork:
hello:
entrypoint: "../hello"
entrypoint: "../../plugins/hello"
helloTo: "preNetwork!"
podName: "hello-pre-network"
postNetwork:
hello:
entrypoint: "../hello"
entrypoint: "../../plugins/hello"
helloTo: "postNetwork!"
podName: "hello-post-network"

File renamed without changes.
124 changes: 124 additions & 0 deletions resources/plugins/hello/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Hello Plugin

## Hello World!
*Hello* is an example plugin to demonstrate the features of Warnet's plugin architecture.

## Usage
In your python virtual environment with Warnet installed and started, create a new Warnet user folder (follow the prompts):

`$ warnet new user_folder`

`$ cd user_folder`

Deploy the *hello* network.

`$ warnet deploy networks/hello`

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.

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.

To view the pods that the *hello* network launched, run `kubectl get all -A`

To view the various "Hello World!" messages, run `kubectl logs pod/POD_NAME`

### A `network.yaml` example
When you initialize a new Warnet network, Warnet will create a new `network.yaml` file. You can modify these files to fit your needs.

For example, example `network.yaml` file includes the *hello* plugin, lightning nodes and the *simln* plugin.

<details>
<summary>network.yaml</summary>

````yaml
nodes:
- name: tank-0000
addnode:
- tank-0001
ln:
lnd: true

- name: tank-0001
addnode:
- tank-0002
ln:
lnd: true

- name: tank-0002
addnode:
- tank-0000
ln:
lnd: true

- name: tank-0003
addnode:
- tank-0000
ln:
lnd: true
lnd:
config: |
bitcoin.timelockdelta=33
channels:
- id:
block: 300
index: 1
target: tank-0004-ln
capacity: 100000
push_amt: 50000

- name: tank-0004
addnode:
- tank-0000
ln:
lnd: true
lnd:
channels:
- id:
block: 300
index: 2
target: tank-0005-ln
capacity: 50000
push_amt: 25000

- name: tank-0005
addnode:
- tank-0000
ln:
lnd: true

plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc)
preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code
hello:
entrypoint: "../../plugins/hello" # This entrypoint path is relative to the network.yaml file
podName: "hello-pre-deploy"
helloTo: "preDeploy!"
postDeploy:
hello:
entrypoint: "../../plugins/hello"
podName: "hello-post-deploy"
helloTo: "postDeploy!"
simln: # You can have multiple plugins per hook
entrypoint: "../../plugins/simln"
activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]'
preNode: # preNode plugins run before each node is deployed
hello:
entrypoint: "../../plugins/hello"
helloTo: "preNode!"
postNode:
hello:
entrypoint: "../../plugins/hello"
helloTo: "postNode!"
preNetwork:
hello:
entrypoint: "../../plugins/hello"
helloTo: "preNetwork!"
podName: "hello-pre-network"
postNetwork:
hello:
entrypoint: "../../plugins/hello"
helloTo: "postNetwork!"
podName: "hello-post-network"
````

</details>

Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
#!/usr/bin/env python3
import json
import logging
from enum import Enum
from pathlib import Path
from typing import Optional

import click
from kubernetes.stream import stream

from warnet.constants import PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent
from warnet.k8s import (
get_default_namespace,
get_static_client,
)
from warnet.process import run_command

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

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


# Plugins look like this in the network.yaml file:
#
# plugins:
# hello:
# podName: "a-pod-name"
# helloTo: "World!"
#
# "podName" and "helloTo" are essentially dictionary keys, and it helps to keep those keys in an
# enum in order to prevent typos.
class PluginContent(Enum):
POD_NAME = ("podName",)
HELLO_TO = "helloTo"


# Warnet uses a python package called "click" to manage terminal interactions with the user.
# To use click, we must declare a click "group" by decorating a function named after the plugin.
# While optional, using click makes it easy for users to interact with your plugin.
@click.group()
@click.pass_context
def hello(ctx):
Expand All @@ -44,6 +58,9 @@ def hello(ctx):
ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir)


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


def _sh(pod, method: str, params: tuple[str, ...]) -> str:
namespace = get_default_namespace()

sclient = get_static_client()
if params:
cmd = [method]
cmd.extend(params)
else:
cmd = [method]
try:
resp = stream(
sclient.connect_get_namespaced_pod_exec,
pod,
namespace,
container=PRIMARY_CONTAINER,
command=cmd,
stderr=True,
stdin=False,
stdout=True,
tty=False,
_preload_content=False,
)
stdout = ""
stderr = ""
while resp.is_open():
resp.update(timeout=1)
if resp.peek_stdout():
stdout_chunk = resp.read_stdout()
stdout += stdout_chunk
if resp.peek_stderr():
stderr_chunk = resp.read_stderr()
stderr += stderr_chunk
return stdout + stderr
except Exception as err:
print(f"Could not execute stream: {err}")


@hello.command(context_settings={"ignore_unknown_options": True})
@click.argument("pod", type=str)
@click.argument("method", type=str)
@click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments
def sh(pod: str, method: str, params: tuple[str, ...]):
"""Run shell commands in a pod"""
print(_sh(pod, method, params))


if __name__ == "__main__":
hello()
21 changes: 0 additions & 21 deletions resources/plugins/simln/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import click
from kubernetes.stream import stream

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

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

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


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


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


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


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


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


# Take note of how click expects us to explicitly declare command line arguments.
@simln.command()
@click.argument("activity", type=str)
@click.pass_context
Expand Down
4 changes: 3 additions & 1 deletion test/simln_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
class SimLNTest(TestBase):
def __init__(self):
super().__init__()
self.network_dir = Path(os.path.dirname(__file__)) / "data" / "plugins" / "ln"
self.network_dir = (
Path(os.path.dirname(__file__)).parent / "resources" / "networks" / "hello"
)
self.plugins_dir = Path(os.path.dirname(__file__)).parent / "resources" / "plugins"
self.simln_exec = "plugins/simln/plugin.py"

Expand Down

0 comments on commit ce1efd6

Please sign in to comment.