mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-03 06:12:14 +08:00
fix(sandbox):deer-flow-provisioner container fails to start in local execution mode (#889)
This commit is contained in:
35
.github/workflows/backend-unit-tests.yml
vendored
Normal file
35
.github/workflows/backend-unit-tests.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Unit Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: unit-tests-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend-unit-tests:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
|
||||||
|
- name: Install backend dependencies
|
||||||
|
working-directory: backend
|
||||||
|
run: uv sync --group dev
|
||||||
|
|
||||||
|
- name: Run unit tests of backend
|
||||||
|
working-directory: backend
|
||||||
|
run: uv run pytest tests/test_provisioner_kubeconfig.py tests/test_docker_sandbox_mode_detection.py
|
||||||
@@ -41,6 +41,8 @@ Docker provides a consistent, isolated environment with all dependencies pre-con
|
|||||||
```bash
|
```bash
|
||||||
make docker-start
|
make docker-start
|
||||||
```
|
```
|
||||||
|
`make docker-start` reads `config.yaml` and starts `provisioner` only for provisioner/Kubernetes sandbox mode.
|
||||||
|
|
||||||
All services will start with hot-reload enabled:
|
All services will start with hot-reload enabled:
|
||||||
- Frontend changes are automatically reloaded
|
- Frontend changes are automatically reloaded
|
||||||
- Backend changes trigger automatic restart
|
- Backend changes trigger automatic restart
|
||||||
@@ -56,7 +58,7 @@ Docker provides a consistent, isolated environment with all dependencies pre-con
|
|||||||
```bash
|
```bash
|
||||||
# Build the custom k3s image (with pre-cached sandbox image)
|
# Build the custom k3s image (with pre-cached sandbox image)
|
||||||
make docker-init
|
make docker-init
|
||||||
# Start all services in Docker (localhost:2026)
|
# Start Docker services (mode-aware, localhost:2026)
|
||||||
make docker-start
|
make docker-start
|
||||||
# Stop Docker development services
|
# Stop Docker development services
|
||||||
make docker-stop
|
make docker-stop
|
||||||
@@ -77,7 +79,8 @@ Docker Compose (deer-flow-dev)
|
|||||||
├→ nginx (port 2026) ← Reverse proxy
|
├→ nginx (port 2026) ← Reverse proxy
|
||||||
├→ web (port 3000) ← Frontend with hot-reload
|
├→ web (port 3000) ← Frontend with hot-reload
|
||||||
├→ api (port 8001) ← Gateway API with hot-reload
|
├→ api (port 8001) ← Gateway API with hot-reload
|
||||||
└→ langgraph (port 2024) ← LangGraph server with hot-reload
|
├→ langgraph (port 2024) ← LangGraph server with hot-reload
|
||||||
|
└→ provisioner (optional, port 8002) ← Started only in provisioner/K8s sandbox mode
|
||||||
```
|
```
|
||||||
|
|
||||||
**Benefits of Docker Development**:
|
**Benefits of Docker Development**:
|
||||||
@@ -238,6 +241,13 @@ cd frontend
|
|||||||
pnpm test
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### PR Regression Checks
|
||||||
|
|
||||||
|
Every pull request runs the backend regression workflow at [.github/workflows/backend-unit-tests.yml](.github/workflows/backend-unit-tests.yml), including:
|
||||||
|
|
||||||
|
- `tests/test_provisioner_kubeconfig.py`
|
||||||
|
- `tests/test_docker_sandbox_mode_detection.py`
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
- **Backend (Python)**: We use `ruff` for linting and formatting
|
- **Backend (Python)**: We use `ruff` for linting and formatting
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -13,7 +13,7 @@ help:
|
|||||||
@echo ""
|
@echo ""
|
||||||
@echo "Docker Development Commands:"
|
@echo "Docker Development Commands:"
|
||||||
@echo " make docker-init - Build the custom k3s image (with pre-cached sandbox image)"
|
@echo " make docker-init - Build the custom k3s image (with pre-cached sandbox image)"
|
||||||
@echo " make docker-start - Start all services in Docker (localhost:2026)"
|
@echo " make docker-start - Start Docker services (mode-aware from config.yaml, localhost:2026)"
|
||||||
@echo " make docker-stop - Stop Docker development services"
|
@echo " make docker-stop - Stop Docker development services"
|
||||||
@echo " make docker-logs - View Docker development logs"
|
@echo " make docker-logs - View Docker development logs"
|
||||||
@echo " make docker-logs-frontend - View Docker frontend logs"
|
@echo " make docker-logs-frontend - View Docker frontend logs"
|
||||||
|
|||||||
@@ -105,9 +105,11 @@ The fastest way to get started with a consistent environment:
|
|||||||
1. **Initialize and start**:
|
1. **Initialize and start**:
|
||||||
```bash
|
```bash
|
||||||
make docker-init # Pull sandbox image (Only once or when image updates)
|
make docker-init # Pull sandbox image (Only once or when image updates)
|
||||||
make docker-start # Start all services and watch for code changes
|
make docker-start # Start services (auto-detects sandbox mode from config.yaml)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`make docker-start` now starts `provisioner` only when `config.yaml` uses provisioner mode (`sandbox.use: src.community.aio_sandbox:AioSandboxProvider` with `provisioner_url`).
|
||||||
|
|
||||||
2. **Access**: http://localhost:2026
|
2. **Access**: http://localhost:2026
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed Docker development guide.
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed Docker development guide.
|
||||||
@@ -142,6 +144,8 @@ DeerFlow supports multiple sandbox execution modes:
|
|||||||
- **Docker Execution** (runs sandbox code in isolated Docker containers)
|
- **Docker Execution** (runs sandbox code in isolated Docker containers)
|
||||||
- **Docker Execution with Kubernetes** (runs sandbox code in Kubernetes pods via provisioner service)
|
- **Docker Execution with Kubernetes** (runs sandbox code in Kubernetes pods via provisioner service)
|
||||||
|
|
||||||
|
For Docker development, service startup follows `config.yaml` sandbox mode. In Local/Docker modes, `provisioner` is not started.
|
||||||
|
|
||||||
See the [Sandbox Configuration Guide](backend/docs/CONFIGURATION.md#sandbox) to configure your preferred mode.
|
See the [Sandbox Configuration Guide](backend/docs/CONFIGURATION.md#sandbox) to configure your preferred mode.
|
||||||
|
|
||||||
#### MCP Server
|
#### MCP Server
|
||||||
@@ -242,6 +246,8 @@ DeerFlow is model-agnostic — it works with any LLM that implements the OpenAI-
|
|||||||
|
|
||||||
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, workflow, and guidelines.
|
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, workflow, and guidelines.
|
||||||
|
|
||||||
|
Regression coverage includes Docker sandbox mode detection and provisioner kubeconfig-path handling tests in `backend/tests/`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is open source and available under the [MIT License](./LICENSE).
|
This project is open source and available under the [MIT License](./LICENSE).
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ DeerFlow is a LangGraph-based AI super agent system with a full-stack architectu
|
|||||||
- **Gateway API** (port 8001): REST API for models, MCP, skills, memory, artifacts, and uploads
|
- **Gateway API** (port 8001): REST API for models, MCP, skills, memory, artifacts, and uploads
|
||||||
- **Frontend** (port 3000): Next.js web interface
|
- **Frontend** (port 3000): Next.js web interface
|
||||||
- **Nginx** (port 2026): Unified reverse proxy entry point
|
- **Nginx** (port 2026): Unified reverse proxy entry point
|
||||||
|
- **Provisioner** (port 8002, optional in Docker dev): Started only when sandbox is configured for provisioner/Kubernetes mode
|
||||||
|
|
||||||
**Project Structure**:
|
**Project Structure**:
|
||||||
```
|
```
|
||||||
@@ -83,8 +84,15 @@ make dev # Run LangGraph server only (port 2024)
|
|||||||
make gateway # Run Gateway API only (port 8001)
|
make gateway # Run Gateway API only (port 8001)
|
||||||
make lint # Lint with ruff
|
make lint # Lint with ruff
|
||||||
make format # Format code with ruff
|
make format # Format code with ruff
|
||||||
|
uv run pytest # Run backend tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Regression tests related to Docker/provisioner behavior:
|
||||||
|
- `tests/test_docker_sandbox_mode_detection.py` (mode detection from `config.yaml`)
|
||||||
|
- `tests/test_provisioner_kubeconfig.py` (kubeconfig file/directory handling)
|
||||||
|
|
||||||
|
CI runs these regression tests for every pull request via [.github/workflows/backend-unit-tests.yml](../.github/workflows/backend-unit-tests.yml).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Agent System
|
### Agent System
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ sandbox:
|
|||||||
provisioner_url: http://provisioner:8002
|
provisioner_url: http://provisioner:8002
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When using Docker development (`make docker-start`), DeerFlow starts the `provisioner` service only if this provisioner mode is configured. In local or plain Docker sandbox modes, `provisioner` is skipped.
|
||||||
|
|
||||||
See [Provisioner Setup Guide](docker/provisioner/README.md) for detailed configuration, prerequisites, and troubleshooting.
|
See [Provisioner Setup Guide](docker/provisioner/README.md) for detailed configuration, prerequisites, and troubleshooting.
|
||||||
|
|
||||||
Choose between local execution or Docker-based isolation:
|
Choose between local execution or Docker-based isolation:
|
||||||
|
|||||||
95
backend/tests/test_docker_sandbox_mode_detection.py
Normal file
95
backend/tests/test_docker_sandbox_mode_detection.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""Regression tests for docker sandbox mode detection logic."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SCRIPT_PATH = REPO_ROOT / "scripts" / "docker.sh"
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_mode_with_config(config_content: str) -> str:
|
||||||
|
"""Write config content into a temp project root and execute detect_sandbox_mode."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_root = Path(tmpdir)
|
||||||
|
(tmp_root / "config.yaml").write_text(config_content)
|
||||||
|
|
||||||
|
command = (
|
||||||
|
f"source '{SCRIPT_PATH}' && "
|
||||||
|
f"PROJECT_ROOT='{tmp_root}' && "
|
||||||
|
"detect_sandbox_mode"
|
||||||
|
)
|
||||||
|
|
||||||
|
output = subprocess.check_output(
|
||||||
|
["bash", "-lc", command],
|
||||||
|
text=True,
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_mode_defaults_to_local_when_config_missing():
|
||||||
|
"""No config file should default to local mode."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
command = (
|
||||||
|
f"source '{SCRIPT_PATH}' && "
|
||||||
|
f"PROJECT_ROOT='{tmpdir}' && "
|
||||||
|
"detect_sandbox_mode"
|
||||||
|
)
|
||||||
|
output = subprocess.check_output(["bash", "-lc", command], text=True).strip()
|
||||||
|
|
||||||
|
assert output == "local"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_mode_local_provider():
|
||||||
|
"""Local sandbox provider should map to local mode."""
|
||||||
|
config = """
|
||||||
|
sandbox:
|
||||||
|
use: src.sandbox.local:LocalSandboxProvider
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
assert _detect_mode_with_config(config) == "local"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_mode_aio_without_provisioner_url():
|
||||||
|
"""AIO sandbox without provisioner_url should map to aio mode."""
|
||||||
|
config = """
|
||||||
|
sandbox:
|
||||||
|
use: src.community.aio_sandbox:AioSandboxProvider
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
assert _detect_mode_with_config(config) == "aio"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_mode_provisioner_with_url():
|
||||||
|
"""AIO sandbox with provisioner_url should map to provisioner mode."""
|
||||||
|
config = """
|
||||||
|
sandbox:
|
||||||
|
use: src.community.aio_sandbox:AioSandboxProvider
|
||||||
|
provisioner_url: http://provisioner:8002
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
assert _detect_mode_with_config(config) == "provisioner"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_mode_ignores_commented_provisioner_url():
|
||||||
|
"""Commented provisioner_url should not activate provisioner mode."""
|
||||||
|
config = """
|
||||||
|
sandbox:
|
||||||
|
use: src.community.aio_sandbox:AioSandboxProvider
|
||||||
|
# provisioner_url: http://provisioner:8002
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
assert _detect_mode_with_config(config) == "aio"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_mode_unknown_provider_falls_back_to_local():
|
||||||
|
"""Unknown sandbox provider should default to local mode."""
|
||||||
|
config = """
|
||||||
|
sandbox:
|
||||||
|
use: custom.module:UnknownProvider
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
assert _detect_mode_with_config(config) == "local"
|
||||||
121
backend/tests/test_provisioner_kubeconfig.py
Normal file
121
backend/tests/test_provisioner_kubeconfig.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""Regression tests for provisioner kubeconfig path handling."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _load_provisioner_module():
|
||||||
|
"""Load docker/provisioner/app.py as an importable test module."""
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
module_path = repo_root / "docker" / "provisioner" / "app.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("provisioner_app_test", module_path)
|
||||||
|
assert spec is not None
|
||||||
|
assert spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def test_wait_for_kubeconfig_rejects_directory(tmp_path):
|
||||||
|
"""Directory mount at kubeconfig path should fail fast with clear error."""
|
||||||
|
provisioner_module = _load_provisioner_module()
|
||||||
|
kubeconfig_dir = tmp_path / "config_dir"
|
||||||
|
kubeconfig_dir.mkdir()
|
||||||
|
|
||||||
|
provisioner_module.KUBECONFIG_PATH = str(kubeconfig_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
provisioner_module._wait_for_kubeconfig(timeout=1)
|
||||||
|
raise AssertionError("Expected RuntimeError for directory kubeconfig path")
|
||||||
|
except RuntimeError as exc:
|
||||||
|
assert "directory" in str(exc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wait_for_kubeconfig_accepts_file(tmp_path):
|
||||||
|
"""Regular file mount should pass readiness wait."""
|
||||||
|
provisioner_module = _load_provisioner_module()
|
||||||
|
kubeconfig_file = tmp_path / "config"
|
||||||
|
kubeconfig_file.write_text("apiVersion: v1\n")
|
||||||
|
|
||||||
|
provisioner_module.KUBECONFIG_PATH = str(kubeconfig_file)
|
||||||
|
|
||||||
|
# Should return immediately without raising.
|
||||||
|
provisioner_module._wait_for_kubeconfig(timeout=1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_k8s_client_rejects_directory_path(tmp_path):
|
||||||
|
"""KUBECONFIG_PATH that resolves to a directory should be rejected."""
|
||||||
|
provisioner_module = _load_provisioner_module()
|
||||||
|
kubeconfig_dir = tmp_path / "config_dir"
|
||||||
|
kubeconfig_dir.mkdir()
|
||||||
|
|
||||||
|
provisioner_module.KUBECONFIG_PATH = str(kubeconfig_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
provisioner_module._init_k8s_client()
|
||||||
|
raise AssertionError("Expected RuntimeError for directory kubeconfig path")
|
||||||
|
except RuntimeError as exc:
|
||||||
|
assert "expected a file" in str(exc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_k8s_client_uses_file_kubeconfig(tmp_path, monkeypatch):
|
||||||
|
"""When file exists, provisioner should load kubeconfig file path."""
|
||||||
|
provisioner_module = _load_provisioner_module()
|
||||||
|
kubeconfig_file = tmp_path / "config"
|
||||||
|
kubeconfig_file.write_text("apiVersion: v1\n")
|
||||||
|
|
||||||
|
called: dict[str, object] = {}
|
||||||
|
|
||||||
|
def fake_load_kube_config(config_file: str):
|
||||||
|
called["config_file"] = config_file
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
provisioner_module.k8s_config,
|
||||||
|
"load_kube_config",
|
||||||
|
fake_load_kube_config,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
provisioner_module.k8s_client,
|
||||||
|
"CoreV1Api",
|
||||||
|
lambda *args, **kwargs: "core-v1",
|
||||||
|
)
|
||||||
|
|
||||||
|
provisioner_module.KUBECONFIG_PATH = str(kubeconfig_file)
|
||||||
|
|
||||||
|
result = provisioner_module._init_k8s_client()
|
||||||
|
|
||||||
|
assert called["config_file"] == str(kubeconfig_file)
|
||||||
|
assert result == "core-v1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_k8s_client_falls_back_to_incluster_when_missing(
|
||||||
|
tmp_path, monkeypatch
|
||||||
|
):
|
||||||
|
"""When kubeconfig file is missing, in-cluster config should be attempted."""
|
||||||
|
provisioner_module = _load_provisioner_module()
|
||||||
|
missing_path = tmp_path / "missing-config"
|
||||||
|
|
||||||
|
calls: dict[str, int] = {"incluster": 0}
|
||||||
|
|
||||||
|
def fake_load_incluster_config():
|
||||||
|
calls["incluster"] += 1
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
provisioner_module.k8s_config,
|
||||||
|
"load_incluster_config",
|
||||||
|
fake_load_incluster_config,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
provisioner_module.k8s_client,
|
||||||
|
"CoreV1Api",
|
||||||
|
lambda *args, **kwargs: "core-v1",
|
||||||
|
)
|
||||||
|
|
||||||
|
provisioner_module.KUBECONFIG_PATH = str(missing_path)
|
||||||
|
|
||||||
|
result = provisioner_module._init_k8s_client()
|
||||||
|
|
||||||
|
assert calls["incluster"] == 1
|
||||||
|
assert result == "core-v1"
|
||||||
@@ -6,11 +6,10 @@
|
|||||||
# - frontend: Frontend Next.js dev server (port 3000)
|
# - frontend: Frontend Next.js dev server (port 3000)
|
||||||
# - gateway: Backend Gateway API (port 8001)
|
# - gateway: Backend Gateway API (port 8001)
|
||||||
# - langgraph: LangGraph server (port 2024)
|
# - langgraph: LangGraph server (port 2024)
|
||||||
# - provisioner: Sandbox provisioner (creates Pods in host Kubernetes)
|
# - provisioner (optional): Sandbox provisioner (creates Pods in host Kubernetes)
|
||||||
#
|
#
|
||||||
# Prerequisites:
|
# Prerequisites:
|
||||||
# - Host machine must have a running Kubernetes cluster (Docker Desktop K8s,
|
# - Kubernetes cluster + kubeconfig are only required when using provisioner mode.
|
||||||
# minikube, kind, etc.) with kubectl configured (~/.kube/config).
|
|
||||||
#
|
#
|
||||||
# Access: http://localhost:2026
|
# Access: http://localhost:2026
|
||||||
|
|
||||||
@@ -20,6 +19,8 @@ services:
|
|||||||
# cluster via the K8s API.
|
# cluster via the K8s API.
|
||||||
# Backend accesses sandboxes directly via host.docker.internal:{NodePort}.
|
# Backend accesses sandboxes directly via host.docker.internal:{NodePort}.
|
||||||
provisioner:
|
provisioner:
|
||||||
|
profiles:
|
||||||
|
- provisioner
|
||||||
build:
|
build:
|
||||||
context: ./provisioner
|
context: ./provisioner
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -55,19 +56,21 @@ services:
|
|||||||
start_period: 15s
|
start_period: 15s
|
||||||
|
|
||||||
# ── Reverse Proxy ──────────────────────────────────────────────────────
|
# ── Reverse Proxy ──────────────────────────────────────────────────────
|
||||||
# Routes API traffic to gateway, langgraph, and provisioner services.
|
# Routes API traffic to gateway/langgraph and (optionally) provisioner.
|
||||||
|
# Select nginx config via NGINX_CONF:
|
||||||
|
# - nginx.local.conf (default): no provisioner route (local/aio modes)
|
||||||
|
# - nginx.conf: includes provisioner route (provisioner mode)
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
container_name: deer-flow-nginx
|
container_name: deer-flow-nginx
|
||||||
ports:
|
ports:
|
||||||
- "2026:2026"
|
- "2026:2026"
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./nginx/${NGINX_CONF:-nginx.local.conf}:/etc/nginx/nginx.conf:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- frontend
|
- frontend
|
||||||
- gateway
|
- gateway
|
||||||
- langgraph
|
- langgraph
|
||||||
- provisioner
|
|
||||||
networks:
|
networks:
|
||||||
- deer-flow-dev
|
- deer-flow-dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'
|
|||||||
The provisioner runs as part of the docker-compose-dev stack:
|
The provisioner runs as part of the docker-compose-dev stack:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start all services including provisioner
|
# Start Docker services (provisioner starts only when config.yaml enables provisioner mode)
|
||||||
make docker-start
|
make docker-start
|
||||||
|
|
||||||
# Or start just the provisioner
|
# Or start just the provisioner
|
||||||
@@ -249,6 +249,18 @@ docker exec deer-flow-gateway curl -s $SANDBOX_URL/v1/sandbox
|
|||||||
- Run `kubectl config view` to verify
|
- Run `kubectl config view` to verify
|
||||||
- Check the volume mount in docker-compose-dev.yaml
|
- Check the volume mount in docker-compose-dev.yaml
|
||||||
|
|
||||||
|
### Issue: "Kubeconfig path is a directory"
|
||||||
|
|
||||||
|
**Cause**: The mounted `KUBECONFIG_PATH` points to a directory instead of a file.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Ensure the compose mount source is a file (e.g., `~/.kube/config`) not a directory
|
||||||
|
- Verify inside container:
|
||||||
|
```bash
|
||||||
|
docker exec deer-flow-provisioner ls -ld /root/.kube/config
|
||||||
|
```
|
||||||
|
- Expected output should indicate a regular file (`-`), not a directory (`d`)
|
||||||
|
|
||||||
### Issue: "Connection refused" to K8s API
|
### Issue: "Connection refused" to K8s API
|
||||||
|
|
||||||
**Cause**: The provisioner can't reach the K8s API server.
|
**Cause**: The provisioner can't reach the K8s API server.
|
||||||
|
|||||||
@@ -80,12 +80,29 @@ def _init_k8s_client() -> k8s_client.CoreV1Api:
|
|||||||
Tries the mounted kubeconfig first, then falls back to in-cluster
|
Tries the mounted kubeconfig first, then falls back to in-cluster
|
||||||
config (useful if the provisioner itself runs inside K8s).
|
config (useful if the provisioner itself runs inside K8s).
|
||||||
"""
|
"""
|
||||||
try:
|
if os.path.exists(KUBECONFIG_PATH):
|
||||||
k8s_config.load_kube_config(config_file=KUBECONFIG_PATH)
|
if os.path.isdir(KUBECONFIG_PATH):
|
||||||
logger.info(f"Loaded kubeconfig from {KUBECONFIG_PATH}")
|
raise RuntimeError(
|
||||||
except Exception:
|
f"KUBECONFIG_PATH points to a directory, expected a file: {KUBECONFIG_PATH}"
|
||||||
logger.warning("Could not load kubeconfig from file, trying in-cluster config")
|
)
|
||||||
k8s_config.load_incluster_config()
|
try:
|
||||||
|
k8s_config.load_kube_config(config_file=KUBECONFIG_PATH)
|
||||||
|
logger.info(f"Loaded kubeconfig from {KUBECONFIG_PATH}")
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to load kubeconfig from {KUBECONFIG_PATH}: {exc}"
|
||||||
|
) from exc
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Kubeconfig not found at {KUBECONFIG_PATH}; trying in-cluster config"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
k8s_config.load_incluster_config()
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Failed to initialize Kubernetes client. "
|
||||||
|
f"No kubeconfig at {KUBECONFIG_PATH}, and in-cluster config is unavailable: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
# When connecting from inside Docker to the host's K8s API, the
|
# When connecting from inside Docker to the host's K8s API, the
|
||||||
# kubeconfig may reference ``localhost`` or ``127.0.0.1``. We
|
# kubeconfig may reference ``localhost`` or ``127.0.0.1``. We
|
||||||
@@ -103,15 +120,27 @@ def _init_k8s_client() -> k8s_client.CoreV1Api:
|
|||||||
|
|
||||||
|
|
||||||
def _wait_for_kubeconfig(timeout: int = 30) -> None:
|
def _wait_for_kubeconfig(timeout: int = 30) -> None:
|
||||||
"""Block until the kubeconfig file is available."""
|
"""Wait for kubeconfig file if configured, then continue with fallback support."""
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
if os.path.exists(KUBECONFIG_PATH):
|
if os.path.exists(KUBECONFIG_PATH):
|
||||||
logger.info(f"Found kubeconfig at {KUBECONFIG_PATH}")
|
if os.path.isfile(KUBECONFIG_PATH):
|
||||||
return
|
logger.info(f"Found kubeconfig file at {KUBECONFIG_PATH}")
|
||||||
|
return
|
||||||
|
if os.path.isdir(KUBECONFIG_PATH):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Kubeconfig path is a directory. "
|
||||||
|
f"Please mount a kubeconfig file at {KUBECONFIG_PATH}."
|
||||||
|
)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Kubeconfig path exists but is not a regular file: {KUBECONFIG_PATH}"
|
||||||
|
)
|
||||||
logger.info(f"Waiting for kubeconfig at {KUBECONFIG_PATH} …")
|
logger.info(f"Waiting for kubeconfig at {KUBECONFIG_PATH} …")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
raise RuntimeError(f"Kubeconfig not found at {KUBECONFIG_PATH} after {timeout}s")
|
logger.warning(
|
||||||
|
f"Kubeconfig not found at {KUBECONFIG_PATH} after {timeout}s; "
|
||||||
|
"will attempt in-cluster Kubernetes config"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_namespace() -> None:
|
def _ensure_namespace() -> None:
|
||||||
|
|||||||
@@ -15,6 +15,51 @@ DOCKER_DIR="$PROJECT_ROOT/docker"
|
|||||||
# Docker Compose command with project name
|
# Docker Compose command with project name
|
||||||
COMPOSE_CMD="docker compose -p deer-flow-dev -f docker-compose-dev.yaml"
|
COMPOSE_CMD="docker compose -p deer-flow-dev -f docker-compose-dev.yaml"
|
||||||
|
|
||||||
|
detect_sandbox_mode() {
|
||||||
|
local config_file="$PROJECT_ROOT/config.yaml"
|
||||||
|
local sandbox_use=""
|
||||||
|
local provisioner_url=""
|
||||||
|
|
||||||
|
if [ ! -f "$config_file" ]; then
|
||||||
|
echo "local"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
sandbox_use=$(awk '
|
||||||
|
/^[[:space:]]*sandbox:[[:space:]]*$/ { in_sandbox=1; next }
|
||||||
|
in_sandbox && /^[^[:space:]#]/ { in_sandbox=0 }
|
||||||
|
in_sandbox && /^[[:space:]]*use:[[:space:]]*/ {
|
||||||
|
line=$0
|
||||||
|
sub(/^[[:space:]]*use:[[:space:]]*/, "", line)
|
||||||
|
print line
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$config_file")
|
||||||
|
|
||||||
|
provisioner_url=$(awk '
|
||||||
|
/^[[:space:]]*sandbox:[[:space:]]*$/ { in_sandbox=1; next }
|
||||||
|
in_sandbox && /^[^[:space:]#]/ { in_sandbox=0 }
|
||||||
|
in_sandbox && /^[[:space:]]*provisioner_url:[[:space:]]*/ {
|
||||||
|
line=$0
|
||||||
|
sub(/^[[:space:]]*provisioner_url:[[:space:]]*/, "", line)
|
||||||
|
print line
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$config_file")
|
||||||
|
|
||||||
|
if [[ "$sandbox_use" == *"src.sandbox.local:LocalSandboxProvider"* ]]; then
|
||||||
|
echo "local"
|
||||||
|
elif [[ "$sandbox_use" == *"src.community.aio_sandbox:AioSandboxProvider"* ]]; then
|
||||||
|
if [ -n "$provisioner_url" ]; then
|
||||||
|
echo "provisioner"
|
||||||
|
else
|
||||||
|
echo "aio"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "local"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Cleanup function for Ctrl+C
|
# Cleanup function for Ctrl+C
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo ""
|
echo ""
|
||||||
@@ -49,10 +94,32 @@ init() {
|
|||||||
|
|
||||||
# Start Docker development environment
|
# Start Docker development environment
|
||||||
start() {
|
start() {
|
||||||
|
local sandbox_mode
|
||||||
|
local nginx_conf
|
||||||
|
local services
|
||||||
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo " Starting DeerFlow Docker Development"
|
echo " Starting DeerFlow Docker Development"
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
sandbox_mode="$(detect_sandbox_mode)"
|
||||||
|
|
||||||
|
if [ "$sandbox_mode" = "provisioner" ]; then
|
||||||
|
nginx_conf="nginx.conf"
|
||||||
|
services="frontend gateway langgraph provisioner nginx"
|
||||||
|
else
|
||||||
|
nginx_conf="nginx.local.conf"
|
||||||
|
services="frontend gateway langgraph nginx"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}Detected sandbox mode: $sandbox_mode${NC}"
|
||||||
|
if [ "$sandbox_mode" = "provisioner" ]; then
|
||||||
|
echo -e "${BLUE}Provisioner enabled (Kubernetes mode).${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${BLUE}Provisioner disabled (not required for this sandbox mode).${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Set DEER_FLOW_ROOT for provisioner if not already set
|
# Set DEER_FLOW_ROOT for provisioner if not already set
|
||||||
if [ -z "$DEER_FLOW_ROOT" ]; then
|
if [ -z "$DEER_FLOW_ROOT" ]; then
|
||||||
@@ -62,7 +129,7 @@ start() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Building and starting containers..."
|
echo "Building and starting containers..."
|
||||||
cd "$DOCKER_DIR" && $COMPOSE_CMD up --build -d --remove-orphans
|
cd "$DOCKER_DIR" && NGINX_CONF="$nginx_conf" $COMPOSE_CMD up --build -d --remove-orphans $services
|
||||||
echo ""
|
echo ""
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo " DeerFlow Docker is starting!"
|
echo " DeerFlow Docker is starting!"
|
||||||
@@ -94,12 +161,16 @@ logs() {
|
|||||||
service="nginx"
|
service="nginx"
|
||||||
echo -e "${BLUE}Viewing nginx logs...${NC}"
|
echo -e "${BLUE}Viewing nginx logs...${NC}"
|
||||||
;;
|
;;
|
||||||
|
--provisioner)
|
||||||
|
service="provisioner"
|
||||||
|
echo -e "${BLUE}Viewing provisioner logs...${NC}"
|
||||||
|
;;
|
||||||
"")
|
"")
|
||||||
echo -e "${BLUE}Viewing all logs...${NC}"
|
echo -e "${BLUE}Viewing all logs...${NC}"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e "${YELLOW}Unknown option: $1${NC}"
|
echo -e "${YELLOW}Unknown option: $1${NC}"
|
||||||
echo "Usage: $0 logs [--frontend|--gateway]"
|
echo "Usage: $0 logs [--frontend|--gateway|--nginx|--provisioner]"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -138,40 +209,48 @@ help() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Commands:"
|
echo "Commands:"
|
||||||
echo " init - Pull the sandbox image (speeds up first Pod startup)"
|
echo " init - Pull the sandbox image (speeds up first Pod startup)"
|
||||||
echo " start - Start all services in Docker (localhost:2026)"
|
echo " start - Start Docker services (auto-detects sandbox mode from config.yaml)"
|
||||||
echo " restart - Restart all running Docker services"
|
echo " restart - Restart all running Docker services"
|
||||||
echo " logs [option] - View Docker development logs"
|
echo " logs [option] - View Docker development logs"
|
||||||
echo " --frontend View frontend logs only"
|
echo " --frontend View frontend logs only"
|
||||||
echo " --gateway View gateway logs only"
|
echo " --gateway View gateway logs only"
|
||||||
|
echo " --nginx View nginx logs only"
|
||||||
|
echo " --provisioner View provisioner logs only"
|
||||||
echo " stop - Stop Docker development services"
|
echo " stop - Stop Docker development services"
|
||||||
echo " help - Show this help message"
|
echo " help - Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main command dispatcher
|
main() {
|
||||||
case "$1" in
|
# Main command dispatcher
|
||||||
init)
|
case "$1" in
|
||||||
init
|
init)
|
||||||
;;
|
init
|
||||||
start)
|
;;
|
||||||
start
|
start)
|
||||||
;;
|
start
|
||||||
restart)
|
;;
|
||||||
restart
|
restart)
|
||||||
;;
|
restart
|
||||||
logs)
|
;;
|
||||||
logs "$2"
|
logs)
|
||||||
;;
|
logs "$2"
|
||||||
stop)
|
;;
|
||||||
stop
|
stop)
|
||||||
;;
|
stop
|
||||||
help|--help|-h|"")
|
;;
|
||||||
help
|
help|--help|-h|"")
|
||||||
;;
|
help
|
||||||
*)
|
;;
|
||||||
echo -e "${YELLOW}Unknown command: $1${NC}"
|
*)
|
||||||
echo ""
|
echo -e "${YELLOW}Unknown command: $1${NC}"
|
||||||
help
|
echo ""
|
||||||
exit 1
|
help
|
||||||
;;
|
exit 1
|
||||||
esac
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||||
|
main "$@"
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user