Skip to content

Commit bb855cf

Browse files
authored
Standalone mode for the exporter (#320)
Allow exporters to serve directly over TCP without a controller, and clients to connect directly without going through a controller ```shell # exporter jmp run --tls-grpc-listener [HOST:]PORT [--tls-grpc-insecure] # client jmp shell --tls-grpc HOST:PORT [--tls-grpc-insecure] ``` Address #44 Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
1 parent 797b3c5 commit bb855cf

23 files changed

Lines changed: 1081 additions & 38 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: jumpstarter.dev/v1alpha1
2+
kind: ExporterConfig
3+
metadata:
4+
name: test-exporter-direct-hooks
5+
namespace: default
6+
export:
7+
power:
8+
type: jumpstarter_driver_power.driver.MockPower
9+
hooks:
10+
beforeLease:
11+
script: |
12+
echo "BEFORE_HOOK_DIRECT: executed"
13+
j power on
14+
echo "BEFORE_HOOK_DIRECT: complete"
15+
timeout: 60
16+
onFailure: warn
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
apiVersion: jumpstarter.dev/v1alpha1
2+
kind: ExporterConfig
3+
metadata:
4+
name: test-exporter-direct-hooks
5+
namespace: default
6+
export:
7+
power:
8+
type: jumpstarter_driver_power.driver.MockPower
9+
hooks:
10+
beforeLease:
11+
script: |
12+
echo "BEFORE_HOOK_DIRECT: executed"
13+
timeout: 60
14+
onFailure: warn
15+
afterLease:
16+
script: |
17+
echo "AFTER_HOOK_DIRECT: executed"
18+
timeout: 60
19+
onFailure: warn
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: jumpstarter.dev/v1alpha1
2+
kind: ExporterConfig
3+
metadata:
4+
name: test-exporter-direct
5+
namespace: default
6+
export:
7+
power:
8+
type: jumpstarter_driver_power.driver.MockPower

e2e/tests-direct-listener.bats

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env bats
2+
# E2E tests for direct TCP listener mode (no controller)
3+
4+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" && pwd)"
5+
EXPORTER_CONFIG="${SCRIPT_DIR}/exporters/exporter-direct-listener.yaml"
6+
LISTENER_PORT=19090
7+
LISTENER_PID=""
8+
9+
setup() {
10+
bats_load_library bats-support
11+
bats_load_library bats-assert
12+
13+
bats_require_minimum_version 1.5.0
14+
}
15+
16+
# Start the exporter in the background.
17+
# $1 - config file (default: $EXPORTER_CONFIG)
18+
# $2 - readiness: "grpc" waits via jmp shell (drains LogStream),
19+
# "port" waits via nc -z (preserves LogStream queue)
20+
# $3 - if set, redirect stderr to ${BATS_TEST_TMPDIR}/exporter.log
21+
# $4 - passphrase (optional)
22+
_start_exporter() {
23+
local config="${1:-$EXPORTER_CONFIG}"
24+
local readiness="${2:-grpc}"
25+
local capture_logs="${3:-}"
26+
local passphrase="${4:-}"
27+
28+
local extra_args=()
29+
if [ -n "$passphrase" ]; then
30+
extra_args+=(--passphrase "$passphrase")
31+
fi
32+
33+
if [ -n "$capture_logs" ]; then
34+
jmp run --exporter-config "$config" \
35+
--tls-grpc-listener "$LISTENER_PORT" \
36+
--tls-grpc-insecure "${extra_args[@]}" 2>"${BATS_TEST_TMPDIR}/exporter.log" &
37+
else
38+
jmp run --exporter-config "$config" \
39+
--tls-grpc-listener "$LISTENER_PORT" \
40+
--tls-grpc-insecure "${extra_args[@]}" &
41+
fi
42+
LISTENER_PID=$!
43+
echo "$LISTENER_PID" > "${BATS_TEST_TMPDIR}/exporter.pid"
44+
45+
local retries=30
46+
if [ "$readiness" = "port" ]; then
47+
# TCP-only check: doesn't drain the LogStream queue, so hook output
48+
# remains buffered for the test command to consume.
49+
while ! nc -z 127.0.0.1 "$LISTENER_PORT" 2>/dev/null; do
50+
retries=$((retries - 1))
51+
if [ "$retries" -le 0 ]; then
52+
echo "Port $LISTENER_PORT did not become available" >&2
53+
return 1
54+
fi
55+
sleep 0.5
56+
done
57+
else
58+
# Full gRPC check: ensures exporter is ready for commands.
59+
# Drains LogStream queue (unsuitable for hook output tests).
60+
local grpc_args=(--tls-grpc "127.0.0.1:${LISTENER_PORT}" --tls-grpc-insecure)
61+
if [ -n "$passphrase" ]; then
62+
grpc_args+=(--passphrase "$passphrase")
63+
fi
64+
while ! jmp shell "${grpc_args[@]}" -- j --help >/dev/null 2>&1; do
65+
retries=$((retries - 1))
66+
if [ "$retries" -le 0 ]; then
67+
echo "Exporter did not become ready in time" >&2
68+
return 1
69+
fi
70+
sleep 0.5
71+
done
72+
fi
73+
}
74+
75+
start_exporter() { _start_exporter "$1" grpc; }
76+
start_exporter_with_logs() { _start_exporter "$1" grpc logs; }
77+
start_exporter_bg() { _start_exporter "$1" port; }
78+
start_exporter_bg_with_logs() { _start_exporter "$1" port logs; }
79+
80+
start_exporter_with_passphrase() { _start_exporter "${2:-$EXPORTER_CONFIG}" grpc "" "$1"; }
81+
82+
stop_exporter() {
83+
if [ -f "${BATS_TEST_TMPDIR}/exporter.pid" ]; then
84+
local pid
85+
pid=$(cat "${BATS_TEST_TMPDIR}/exporter.pid")
86+
if [ -n "$pid" ] && ps -p "$pid" > /dev/null 2>&1; then
87+
kill "$pid" 2>/dev/null || true
88+
wait "$pid" 2>/dev/null || true
89+
fi
90+
rm -f "${BATS_TEST_TMPDIR}/exporter.pid"
91+
fi
92+
}
93+
94+
teardown() {
95+
stop_exporter
96+
}
97+
98+
@test "direct listener: exporter starts and client can connect" {
99+
start_exporter
100+
101+
run jmp shell --tls-grpc "127.0.0.1:${LISTENER_PORT}" --tls-grpc-insecure -- j power on
102+
assert_success
103+
}
104+
105+
@test "direct listener: client can call multiple driver methods" {
106+
start_exporter
107+
108+
run jmp shell --tls-grpc "127.0.0.1:${LISTENER_PORT}" --tls-grpc-insecure -- j power on
109+
assert_success
110+
111+
run jmp shell --tls-grpc "127.0.0.1:${LISTENER_PORT}" --tls-grpc-insecure -- j power off
112+
assert_success
113+
}
114+
115+
@test "direct listener: client without --tls-grpc-insecure fails against insecure server" {
116+
start_exporter
117+
118+
run jmp shell --tls-grpc "127.0.0.1:${LISTENER_PORT}" -- j power on
119+
assert_failure
120+
}
121+
122+
@test "direct listener hooks: beforeLease hook executes and j commands work" {
123+
# Use start_exporter_bg (TCP-only readiness check) to avoid draining
124+
# the LogStream queue before the test command connects.
125+
start_exporter_bg "${SCRIPT_DIR}/exporters/exporter-direct-hooks-before.yaml"
126+
127+
run jmp shell --tls-grpc "127.0.0.1:${LISTENER_PORT}" --tls-grpc-insecure \
128+
--exporter-logs -- j power off
129+
assert_success
130+
assert_output --partial "BEFORE_HOOK_DIRECT: executed"
131+
assert_output --partial "BEFORE_HOOK_DIRECT: complete"
132+
}
133+
134+
@test "direct listener hooks: afterLease hook runs on exporter shutdown" {
135+
start_exporter_bg_with_logs "${SCRIPT_DIR}/exporters/exporter-direct-hooks-both.yaml"
136+
137+
run jmp shell --tls-grpc "127.0.0.1:${LISTENER_PORT}" --tls-grpc-insecure \
138+
--exporter-logs -- j power on
139+
assert_success
140+
assert_output --partial "BEFORE_HOOK_DIRECT: executed"
141+
142+
# Stop the exporter (SIGTERM triggers _cleanup_after_lease).
143+
# stop_exporter waits for the process to exit, so the log is complete.
144+
stop_exporter
145+
146+
# afterLease hook output should appear in the exporter's stderr log
147+
run cat "${BATS_TEST_TMPDIR}/exporter.log"
148+
assert_output --partial "AFTER_HOOK_DIRECT: executed"
149+
}
150+
151+
@test "direct listener passphrase: correct passphrase connects" {
152+
start_exporter_with_passphrase "my-secret"
153+
154+
run jmp shell --tls-grpc "127.0.0.1:${LISTENER_PORT}" --tls-grpc-insecure \
155+
--passphrase "my-secret" -- j power on
156+
assert_success
157+
}
158+
159+
@test "direct listener passphrase: wrong passphrase is rejected" {
160+
start_exporter_with_passphrase "my-secret"
161+
162+
run jmp shell --tls-grpc "127.0.0.1:${LISTENER_PORT}" --tls-grpc-insecure \
163+
--passphrase "wrong" -- j power on
164+
assert_failure
165+
}
166+
167+
@test "direct listener passphrase: missing passphrase is rejected" {
168+
start_exporter_with_passphrase "my-secret"
169+
170+
run jmp shell --tls-grpc "127.0.0.1:${LISTENER_PORT}" --tls-grpc-insecure \
171+
-- j power on
172+
assert_failure
173+
}

python/docs/source/getting-started/configuration/files.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ drivers:
5050

5151
**Environment Variables**:
5252

53-
- `JUMPSTARTER_GRPC_INSECURE` - Set to `1` to disable TLS verification globally
53+
- `JUMPSTARTER_GRPC_INSECURE` / `JMP_GRPC_INSECURE` - Set to `1` to disable TLS verification globally
5454
- `JMP_CLIENT_CONFIG` - Path to a client configuration file
5555
- `JMP_CLIENT` - Name of a registered client config
5656
- `JMP_NAMESPACE` - Namespace in the controller
@@ -118,7 +118,7 @@ boundaries. See [Hooks](../../introduction/hooks.md) for full details on
118118
hook configuration, environment variables, and failure handling.
119119

120120
**Environment Variables**:
121-
- `JUMPSTARTER_GRPC_INSECURE` - Set to `1` to disable TLS verification
121+
- `JUMPSTARTER_GRPC_INSECURE` / `JMP_GRPC_INSECURE` - Set to `1` to disable TLS verification
122122
- `JMP_ENDPOINT` - gRPC endpoint (overrides config file)
123123
- `JMP_TOKEN` - Auth token (overrides config file)
124124
- `JMP_NAMESPACE` - Namespace in the controller

python/docs/source/getting-started/guides/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ development workflow. The guides cover:
55

66
- [Setup Local Mode](setup-local-mode.md): Running Jumpstarter in local mode for
77
individual development
8+
- [Setup Direct Mode](setup-direct-mode.md): Connecting a client directly to an
9+
exporter over TCP, without a controller
810
- [Setup Distributed Mode](setup-distributed-mode.md): Configuring Jumpstarter
911
for team environments with shared resources
1012
- [Examples](examples.md): Practical examples of Jumpstarter usage in common
@@ -19,6 +21,7 @@ development workflow. The guides cover:
1921
:maxdepth: 1
2022
:hidden:
2123
setup-local-mode.md
24+
setup-direct-mode.md
2225
setup-distributed-mode.md
2326
examples.md
2427
integration-patterns.md
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Setup Direct Mode
2+
3+
This guide shows you how to run a Jumpstarter exporter that clients connect to
4+
directly over TCP — no controller or Kubernetes cluster required.
5+
6+
Direct mode is useful when you want to expose hardware on one machine to clients
7+
on another, without setting up a controller.
8+
9+
```{note}
10+
Direct mode skips the controller's lease management. Only one client should
11+
connect at a time. For shared, multi-user environments use
12+
[distributed mode](setup-distributed-mode.md) instead.
13+
```
14+
15+
## Instructions
16+
17+
### Create an Exporter Configuration
18+
19+
Unlike distributed mode, you don't need `endpoint` or `token` fields — there
20+
is no controller to register with.
21+
22+
Create `example-direct.yaml`:
23+
24+
```yaml
25+
apiVersion: jumpstarter.dev/v1alpha1
26+
kind: ExporterConfig
27+
metadata:
28+
namespace: default
29+
name: example-direct
30+
export:
31+
power:
32+
type: jumpstarter_driver_power.driver.MockPower
33+
hooks:
34+
beforeLease:
35+
script: |
36+
echo "Exporter ready"
37+
j power on
38+
timeout: 30
39+
afterLease:
40+
script: |
41+
j power off
42+
timeout: 30
43+
```
44+
45+
The `hooks` section is optional. `beforeLease` runs once when the exporter
46+
starts (before any client connects), and `afterLease` runs on shutdown. Hook
47+
scripts can use `j` commands to interact with the drivers.
48+
49+
### Start the Exporter
50+
51+
Run the exporter and tell it to listen on a TCP port with `--tls-grpc-listener`:
52+
53+
```console
54+
$ jmp run --exporter-config example-direct.yaml \
55+
--tls-grpc-listener 0.0.0.0:19090 \
56+
--tls-grpc-insecure
57+
```
58+
59+
The `--tls-grpc-insecure` flag disables TLS, which is convenient for local
60+
development. For production use, provide `--tls-cert` and `--tls-key` instead.
61+
62+
To require a passphrase from connecting clients, add `--passphrase`:
63+
64+
```console
65+
$ jmp run --exporter-config example-direct.yaml \
66+
--tls-grpc-listener 0.0.0.0:19090 \
67+
--tls-grpc-insecure \
68+
--passphrase my-secret
69+
```
70+
71+
### Connect a Client
72+
73+
```console
74+
$ jmp shell --tls-grpc <HOST>:19090 --tls-grpc-insecure
75+
```
76+
77+
If the exporter requires a passphrase:
78+
79+
```console
80+
$ jmp shell --tls-grpc <HOST>:19090 --tls-grpc-insecure --passphrase my-secret
81+
```
82+
83+
Replace `<HOST>` with the exporter machine's IP address or hostname. Once
84+
connected, interact with the exporter using `j` commands:
85+
86+
```console
87+
$ j power on
88+
$ j power off
89+
```
90+
91+
You can also pass a command directly without opening an interactive shell:
92+
93+
```console
94+
$ jmp shell --tls-grpc <HOST>:19090 --tls-grpc-insecure -- j power on
95+
```

0 commit comments

Comments
 (0)