diff --git a/apps/rlark/docs/examples/quickstart-dp.sh b/apps/rlark/docs/examples/quickstart-dp.sh index 8502022..ac77044 100644 --- a/apps/rlark/docs/examples/quickstart-dp.sh +++ b/apps/rlark/docs/examples/quickstart-dp.sh @@ -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 @@ -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" @@ -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 @@ -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" @@ -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 \ No newline at end of file +done diff --git a/apps/rlark/docs/examples/tests/test_quickstart_dp.py b/apps/rlark/docs/examples/tests/test_quickstart_dp.py new file mode 100644 index 0000000..52b1323 --- /dev/null +++ b/apps/rlark/docs/examples/tests/test_quickstart_dp.py @@ -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() diff --git a/apps/rlark/docs/quickstart.md b/apps/rlark/docs/quickstart.md index db407f6..a6c9557 100644 --- a/apps/rlark/docs/quickstart.md +++ b/apps/rlark/docs/quickstart.md @@ -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) @@ -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 \ No newline at end of file +- Read [RL Training Best Practices](user-guide/best-practices.md) for an end-to-end walkthrough diff --git a/apps/rlark/docs/zh/quickstart.md b/apps/rlark/docs/zh/quickstart.md index 7f24f52..d4a2d93 100644 --- a/apps/rlark/docs/zh/quickstart.md +++ b/apps/rlark/docs/zh/quickstart.md @@ -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) @@ -356,4 +360,4 @@ spec: - 阅读 [管理员指南](admin-guide/index.md) 了解生产部署、集群接入与运维 - 阅读 [核心概念](concepts.md) 了解资源模型和命名约定 - 阅读 [部署指南](deployment.md) 了解生产环境部署和真机设备纳管 -- 阅读 [RL 训练最佳实践](user-guide/best-practices.md) 查看端到端的完整操作示例 \ No newline at end of file +- 阅读 [RL 训练最佳实践](user-guide/best-practices.md) 查看端到端的完整操作示例