Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions apps/rlark/docs/examples/quickstart-dp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,32 @@ command -v docker >/dev/null 2>&1 || err "docker is required"
command -v kind >/dev/null 2>&1 || err "kind is required"
command -v kubectl >/dev/null 2>&1 || err "kubectl is required"
command -v jq >/dev/null 2>&1 || err "jq is required"
command -v curl >/dev/null 2>&1 || err "curl is required"

# Verify control plane is running
if [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:9000/api/v1/rlinf.io/v1alpha1/nodes 2>/dev/null)" != "200" ]; then
err "Control plane not reachable at http://localhost:9000. Run quickstart-cp.sh first."
# Authenticate before creating or deleting kind clusters. An existing admin
# token may be supplied; otherwise use the local quickstart credentials.
if [ -z "${RLARK_TOKEN:-}" ]; then
ADMIN_PASSWORD=$(kubectl --kubeconfig /tmp/rlark/admin.kubeconfig --context root-shard \
-n default get secret rlark-ui-auth -o json | \
jq -er '.data["admin-password"] | select(type == "string" and length > 0) | @base64d') || \
err "Cannot read quickstart admin credentials; supply an admin JWT through RLARK_TOKEN."
RLARK_TOKEN=$(jq -n --arg password "$ADMIN_PASSWORD" '{username:"admin",password:$password}' | \
curl -fsS --connect-timeout 5 --max-time 15 \
-H 'Content-Type: application/json' --data-binary @- \
http://localhost:9000/api/v1/auth/login | \
jq -er 'select(.role == "admin") | .token | select(type == "string" and length > 0)') || \
err "Gateway login failed; check rlark-gateway logs and the UI credentials."
unset ADMIN_PASSWORD
fi

gateway_curl() {
curl -fsS --connect-timeout 5 --max-time 30 \
-H "Authorization: Bearer $RLARK_TOKEN" "$@"
}

# Verify the authenticated API, rather than treating an anonymous 401 as downtime.
gateway_curl http://localhost:9000/api/v1/rlinf.io/v1alpha1/nodes >/dev/null || \
err "Authenticated node query failed; check Gateway logs and token validity."
ok "Control plane is reachable"

# Get registry IP
Expand Down Expand Up @@ -129,9 +150,12 @@ log "Step 3: Requesting agent certificates..."
for i in $(seq 1 $CLUSTER_COUNT); do
CID="${CLUSTER_IDS[$((i-1))]}"
log "Requesting cert for cluster-id: $CID"
curl -s -X POST "http://localhost:9000/api/v1/certificates/agent" \
gateway_curl -X POST "http://localhost:9000/api/v1/certificates/agent" \
-H "Content-Type: application/json" \
-d "{\"cluster_id\":\"${CID}\"}" > "/tmp/rlark/agent-cert-$i.json"
-d "$(jq -n --arg cid "$CID" '{cluster_id:$cid}')" > "/tmp/rlark/agent-cert-$i.json" || \
err "Agent certificate request failed for $CID"
jq -e 'all(.ca_cert, .agent_cert, .agent_key; type == "string" and length > 0)' \
"/tmp/rlark/agent-cert-$i.json" >/dev/null || err "Incomplete agent certificate response for $CID"
jq -r .ca_cert "/tmp/rlark/agent-cert-$i.json" > "/tmp/rlark/ca-$i.pem"
jq -r .agent_cert "/tmp/rlark/agent-cert-$i.json" > "/tmp/rlark/cert-$i.pem"
jq -r .agent_key "/tmp/rlark/agent-cert-$i.json" > "/tmp/rlark/key-$i.pem"
Expand Down Expand Up @@ -197,7 +221,7 @@ log "Step 5: Verifying node registration (waiting for agents to report)..."
for attempt in $(seq 1 30); do
REGISTERED=0
for CID in "${CLUSTER_IDS[@]}"; do
if curl -fsS "http://localhost:9000/api/v1/rlinf.io/v1alpha1/nodes" | \
if gateway_curl "http://localhost:9000/api/v1/rlinf.io/v1alpha1/nodes" | \
jq -e --arg cid "rlark-${CID}" '.items[] | select(.metadata.labels["rlark.io/cluster-id"] == $cid)' >/dev/null; then
REGISTERED=$((REGISTERED + 1))
fi
Expand All @@ -206,7 +230,7 @@ for attempt in $(seq 1 30); do
sleep 2
done
[ "${REGISTERED:-0}" -eq "$CLUSTER_COUNT" ] || err "Only ${REGISTERED:-0}/$CLUSTER_COUNT expected nodes registered"
curl -fsS "http://localhost:9000/api/v1/rlinf.io/v1alpha1/nodes" | \
gateway_curl "http://localhost:9000/api/v1/rlinf.io/v1alpha1/nodes" | \
jq -r '.items[] | " \(.metadata.name) cluster-id=\(.metadata.labels["rlark.io/cluster-id"])"'
ok "All $CLUSTER_COUNT nodes verified"

Expand Down Expand Up @@ -237,4 +261,4 @@ echo ""
echo "Cleanup:"
for i in $(seq 1 $CLUSTER_COUNT); do
echo " kind delete cluster --name ${CLUSTER_NAME_PREFIX}-$i"
done
done
123 changes: 123 additions & 0 deletions apps/rlark/docs/examples/tests/test_quickstart_dp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Exercise the data-plane script without Docker, Kubernetes, or network access.

Run: python3 -m unittest discover -s apps/rlark/docs/examples/tests
"""

import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest


EXAMPLES = Path(__file__).resolve().parents[1]
STUB = r'''
import base64, json, os, pathlib, sys
name = pathlib.Path(sys.argv[0]).name
args = sys.argv[1:]
with open(os.environ["CALL_LOG"], "a") as log:
log.write(json.dumps([name, args]) + "\n")
failure = os.environ.get("FAILURE", "")
if name == "kubectl":
if "secret" in args and "get" in args:
if failure == "secret":
sys.exit(1)
print(json.dumps({"data": {"admin-password": base64.b64encode(b'password"with\\quotes').decode()}}))
elif "nodes" in args and "get" in args:
print("test-node")
elif name == "docker" and args[0] == "inspect":
print("test-network" if args[1] == "kcp" else "172.17.0.2")
elif name == "kind" and args[:2] == ["get", "kubeconfig"]:
print("test-kubeconfig")
elif name == "curl":
url = next(a for a in args if a.startswith("http://"))
if url.endswith("/login"):
assert json.load(sys.stdin) == {"username": "admin", "password": 'password"with\\quotes'}
if failure == "login":
sys.exit(22)
print(json.dumps({"role": "admin", "token": "test-token"}))
else:
assert "Authorization: Bearer test-token" in args, "missing authentication"
if url.endswith("/certificates/agent"):
assert json.loads(args[args.index("-d") + 1]) == {"cluster_id": "my-cluster"}
if failure == "certificate":
sys.exit(22)
print(json.dumps({} if failure == "incomplete" else {
"ca_cert": "ca", "agent_cert": "cert", "agent_key": "key"}))
else:
if failure == "nodes":
sys.exit(22)
print(json.dumps({"items": [{"metadata": {"name": "test-node", "labels": {
"rlark.io/cluster-id": "rlark-my-cluster"}}}]}))
'''


@unittest.skipUnless(shutil.which("jq"), "jq is required")
class QuickstartDataPlaneTests(unittest.TestCase):
def run_script(self, failure="", token=None):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
bin_dir = root / "bin"
bin_dir.mkdir()
for name in ("docker", "kind", "kubectl", "curl", "sleep"):
stub = bin_dir / name
stub.write_text(f"#!{sys.executable}\n" + STUB)
stub.chmod(0o755)
runtime = root / "runtime"
(runtime / "rlark").mkdir(parents=True)
for name in ("agent-rbac.yaml", "agent-deploy.yaml"):
shutil.copy(EXAMPLES / name, root / name)
script = root / "quickstart-dp.sh"
# Redirect only scratch paths; run the actual script and all its stages.
script.write_text((EXAMPLES / script.name).read_text().replace("/tmp/", f"{runtime}/"))
log = root / "calls.jsonl"
env = dict(os.environ, PATH=f"{bin_dir}:{os.environ['PATH']}",
CALL_LOG=str(log), FAILURE=failure)
env.pop("RLARK_TOKEN", None)
if token:
env["RLARK_TOKEN"] = token
result = subprocess.run(
["bash", str(script), "--cluster-id", "my-cluster"],
env=env, capture_output=True, text=True, timeout=20,
)
calls = [json.loads(line) for line in log.read_text().splitlines()]
return result, calls

def test_login_and_all_authenticated_requests(self):
result, calls = self.run_script()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
urls = [next(a for a in args if a.startswith("http://"))
for name, args in calls if name == "curl"]
self.assertEqual(sum(url.endswith("/nodes") for url in urls), 3)
self.assertTrue(any(url.endswith("/certificates/agent") for url in urls))
self.assertNotIn("test-token", result.stdout + result.stderr)

def test_existing_token_skips_password_lookup_and_login(self):
result, calls = self.run_script(token="test-token")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertFalse(any(name == "kubectl" and "get" in args and "secret" in args
for name, args in calls))
self.assertFalse(any("http://localhost:9000/api/v1/auth/login" in args
for _, args in calls))

def test_authentication_failures_precede_cluster_mutations(self):
for failure in ("secret", "login", "nodes"):
with self.subTest(failure=failure):
result, calls = self.run_script(failure)
self.assertNotEqual(result.returncode, 0)
self.assertFalse(any(name in ("kind", "docker") for name, _ in calls))

def test_certificate_errors_stop_before_agent_deployment(self):
for failure in ("certificate", "incomplete"):
with self.subTest(failure=failure):
result, calls = self.run_script(failure)
self.assertNotEqual(result.returncode, 0)
self.assertFalse(any(name == "kubectl" and "apply" in args
for name, args in calls))


if __name__ == "__main__":
unittest.main()
7 changes: 6 additions & 1 deletion apps/rlark/docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ Run the data plane script with the cluster name from step 2:
bash apps/rlark/docs/examples/quickstart-dp.sh --cluster-id my-cluster
```

Run this on the same machine as the control-plane quickstart script. It automatically
logs in with the administrator credentials generated by quickstart, or uses an
administrator access token supplied through `RLARK_TOKEN`.
Rerunning recreates the corresponding kind clusters; save any data you need first.

This script:
- Creates a kind cluster
- Requests an agent certificate from the Gateway for the given cluster-id (no need for the `deploy-conf.yaml` from the UI)
Expand Down Expand Up @@ -355,4 +360,4 @@ Client Pod (cluster-2) Server Pod (cluster-1)
- Read the [Administrator Guide](admin-guide/index.md) for production deployment and operations
- Read [Core Concepts](concepts.md) for the resource model and naming conventions
- Read [Deployment Guide](deployment.md) for production deployment and real device onboarding
- Read [RL Training Best Practices](user-guide/best-practices.md) for an end-to-end walkthrough
- Read [RL Training Best Practices](user-guide/best-practices.md) for an end-to-end walkthrough
6 changes: 5 additions & 1 deletion apps/rlark/docs/zh/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ bash apps/rlark/docs/examples/quickstart-cp.sh --no-ui
bash apps/rlark/docs/examples/quickstart-dp.sh --cluster-id my-cluster
```

在运行控制面 quickstart 脚本的同一台机器上执行。脚本自动使用管理员凭据登录,
也支持通过环境变量 `RLARK_TOKEN` 提供管理员访问令牌。
重复运行会重建对应的 kind 集群,请确认其中没有需要保留的数据。

脚本会:
- 创建 kind 集群
- 根据集群 ID 向 Gateway 自动申请 Agent 证书(无需 UI 中的 deploy-conf.yaml)
Expand Down Expand Up @@ -356,4 +360,4 @@ spec:
- 阅读 [管理员指南](admin-guide/index.md) 了解生产部署、集群接入与运维
- 阅读 [核心概念](concepts.md) 了解资源模型和命名约定
- 阅读 [部署指南](deployment.md) 了解生产环境部署和真机设备纳管
- 阅读 [RL 训练最佳实践](user-guide/best-practices.md) 查看端到端的完整操作示例
- 阅读 [RL 训练最佳实践](user-guide/best-practices.md) 查看端到端的完整操作示例