mirror of
https://github.com/katanemo/plano.git
synced 2026-05-21 13:55:15 +02:00
Make native mode default, auto-download pre-compiled binaries
- Flip --native to --docker on up/down commands (native is now default) - Add ensure_wasm_plugins() and ensure_brightstaff_binary() to auto-download from GitHub releases - Add _find_config_dir() to support pip-installed usage without repo checkout - Bundle config templates in wheel via pyproject.toml force-include - Add publish-binaries.yml CI workflow for release binary uploads - Update docs to reflect native-first experience
This commit is contained in:
parent
39a5c21209
commit
edfd237111
7 changed files with 299 additions and 91 deletions
|
|
@ -16,3 +16,6 @@ PLANO_PLUGINS_DIR = os.path.join(PLANO_HOME, "plugins")
|
|||
ENVOY_VERSION = "v1.37.0" # keep in sync with Dockerfile ARG ENVOY_VERSION
|
||||
NATIVE_PID_FILE = os.path.join(PLANO_RUN_DIR, "plano.pid")
|
||||
DEFAULT_NATIVE_OTEL_TRACING_GRPC_ENDPOINT = "http://localhost:4317"
|
||||
|
||||
PLANO_GITHUB_REPO = "katanemo/archgw"
|
||||
PLANO_RELEASE_BASE_URL = f"https://github.com/{PLANO_GITHUB_REPO}/releases/download"
|
||||
|
|
|
|||
|
|
@ -262,12 +262,12 @@ def build(native):
|
|||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--native",
|
||||
"--docker",
|
||||
default=False,
|
||||
help="Run Plano natively without Docker (requires prior 'planoai build --native').",
|
||||
help="Run Plano inside Docker instead of natively.",
|
||||
is_flag=True,
|
||||
)
|
||||
def up(file, path, foreground, with_tracing, tracing_port, native):
|
||||
def up(file, path, foreground, with_tracing, tracing_port, docker):
|
||||
"""Starts Plano."""
|
||||
from rich.status import Status
|
||||
|
||||
|
|
@ -284,11 +284,11 @@ def up(file, path, foreground, with_tracing, tracing_port, native):
|
|||
)
|
||||
sys.exit(1)
|
||||
|
||||
if native:
|
||||
if not docker:
|
||||
from planoai.native_runner import native_validate_config
|
||||
|
||||
with Status(
|
||||
"[dim]Validating configuration (native)[/dim]",
|
||||
"[dim]Validating configuration[/dim]",
|
||||
spinner="dots",
|
||||
spinner_style="dim",
|
||||
):
|
||||
|
|
@ -303,7 +303,9 @@ def up(file, path, foreground, with_tracing, tracing_port, native):
|
|||
sys.exit(1)
|
||||
else:
|
||||
with Status(
|
||||
"[dim]Validating configuration[/dim]", spinner="dots", spinner_style="dim"
|
||||
"[dim]Validating configuration (Docker)[/dim]",
|
||||
spinner="dots",
|
||||
spinner_style="dim",
|
||||
):
|
||||
(
|
||||
validation_return_code,
|
||||
|
|
@ -321,9 +323,9 @@ def up(file, path, foreground, with_tracing, tracing_port, native):
|
|||
|
||||
# Set up environment
|
||||
default_otel = (
|
||||
DEFAULT_NATIVE_OTEL_TRACING_GRPC_ENDPOINT
|
||||
if native
|
||||
else DEFAULT_OTEL_TRACING_GRPC_ENDPOINT
|
||||
DEFAULT_OTEL_TRACING_GRPC_ENDPOINT
|
||||
if docker
|
||||
else DEFAULT_NATIVE_OTEL_TRACING_GRPC_ENDPOINT
|
||||
)
|
||||
env_stage = {
|
||||
"OTEL_TRACING_GRPC_ENDPOINT": default_otel,
|
||||
|
|
@ -394,13 +396,13 @@ def up(file, path, foreground, with_tracing, tracing_port, native):
|
|||
sys.exit(1)
|
||||
|
||||
# Update the OTEL endpoint so the gateway sends traces to the right port
|
||||
tracing_host = "localhost" if native else "host.docker.internal"
|
||||
tracing_host = "host.docker.internal" if docker else "localhost"
|
||||
otel_endpoint = f"http://{tracing_host}:{tracing_port}"
|
||||
env_stage["OTEL_TRACING_GRPC_ENDPOINT"] = otel_endpoint
|
||||
|
||||
env.update(env_stage)
|
||||
try:
|
||||
if native:
|
||||
if not docker:
|
||||
from planoai.native_runner import start_native
|
||||
|
||||
start_native(
|
||||
|
|
@ -426,27 +428,28 @@ def up(file, path, foreground, with_tracing, tracing_port, native):
|
|||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--native",
|
||||
"--docker",
|
||||
default=False,
|
||||
help="Stop a natively-running Plano instance.",
|
||||
help="Stop a Docker-based Plano instance.",
|
||||
is_flag=True,
|
||||
)
|
||||
def down(native):
|
||||
def down(docker):
|
||||
"""Stops Plano."""
|
||||
console = _console()
|
||||
_print_cli_header(console)
|
||||
|
||||
if native:
|
||||
if not docker:
|
||||
from planoai.native_runner import stop_native
|
||||
|
||||
with console.status(
|
||||
f"[{PLANO_COLOR}]Shutting down Plano (native)...[/{PLANO_COLOR}]",
|
||||
f"[{PLANO_COLOR}]Shutting down Plano...[/{PLANO_COLOR}]",
|
||||
spinner="dots",
|
||||
):
|
||||
stop_native()
|
||||
else:
|
||||
with console.status(
|
||||
f"[{PLANO_COLOR}]Shutting down Plano...[/{PLANO_COLOR}]", spinner="dots"
|
||||
f"[{PLANO_COLOR}]Shutting down Plano (Docker)...[/{PLANO_COLOR}]",
|
||||
spinner="dots",
|
||||
):
|
||||
stop_docker_container()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import sys
|
|||
import tarfile
|
||||
import tempfile
|
||||
|
||||
import planoai
|
||||
from planoai.consts import (
|
||||
ENVOY_VERSION,
|
||||
PLANO_BIN_DIR,
|
||||
PLANO_PLUGINS_DIR,
|
||||
PLANO_RELEASE_BASE_URL,
|
||||
)
|
||||
from planoai.utils import find_repo_root, getLogger
|
||||
|
||||
|
|
@ -15,7 +18,7 @@ log = getLogger(__name__)
|
|||
|
||||
|
||||
def _get_platform_slug():
|
||||
"""Return the platform slug for Envoy binary downloads."""
|
||||
"""Return the platform slug for binary downloads."""
|
||||
system = platform.system().lower()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
|
|
@ -30,7 +33,7 @@ def _get_platform_slug():
|
|||
if system == "darwin" and machine == "x86_64":
|
||||
print(
|
||||
"Error: macOS x86_64 (Intel) is not supported. "
|
||||
"Pre-built Envoy binaries are only available for Apple Silicon (arm64)."
|
||||
"Pre-built binaries are only available for Apple Silicon (arm64)."
|
||||
)
|
||||
sys.exit(1)
|
||||
print(
|
||||
|
|
@ -42,6 +45,20 @@ def _get_platform_slug():
|
|||
return slug
|
||||
|
||||
|
||||
def _download_file(url, dest):
|
||||
"""Download a file from *url* to *dest* using curl."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["curl", "-fSL", "-o", dest, url],
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error downloading: {e}")
|
||||
print(f"URL: {url}")
|
||||
print("Please check your internet connection and try again.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_envoy_binary():
|
||||
"""Download Envoy binary if not already present or version changed. Returns path to binary."""
|
||||
envoy_path = os.path.join(PLANO_BIN_DIR, "envoy")
|
||||
|
|
@ -78,10 +95,7 @@ def ensure_envoy_binary():
|
|||
tmp_path = tmp.name
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["curl", "-fSL", "-o", tmp_path, url],
|
||||
check=True,
|
||||
)
|
||||
_download_file(url, tmp_path)
|
||||
|
||||
print("Extracting Envoy binary...")
|
||||
with tarfile.open(tmp_path, "r:xz") as tar:
|
||||
|
|
@ -114,16 +128,86 @@ def ensure_envoy_binary():
|
|||
print(f"Envoy {ENVOY_VERSION} installed at {envoy_path}")
|
||||
return envoy_path
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error downloading Envoy: {e}")
|
||||
print(f"URL: {url}")
|
||||
print("Please check your internet connection and try again.")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
os.unlink(tmp_path)
|
||||
|
||||
|
||||
def ensure_wasm_plugins():
|
||||
"""Download WASM plugins if not cached or version changed. Returns (prompt_gateway_path, llm_gateway_path)."""
|
||||
version = planoai.__version__
|
||||
version_path = os.path.join(PLANO_PLUGINS_DIR, "wasm.version")
|
||||
|
||||
prompt_gw_path = os.path.join(PLANO_PLUGINS_DIR, "prompt_gateway.wasm")
|
||||
llm_gw_path = os.path.join(PLANO_PLUGINS_DIR, "llm_gateway.wasm")
|
||||
|
||||
if os.path.exists(prompt_gw_path) and os.path.exists(llm_gw_path):
|
||||
if os.path.exists(version_path):
|
||||
with open(version_path, "r") as f:
|
||||
cached_version = f.read().strip()
|
||||
if cached_version == version:
|
||||
log.info(f"WASM plugins {version} found at {PLANO_PLUGINS_DIR}")
|
||||
return prompt_gw_path, llm_gw_path
|
||||
print(
|
||||
f"WASM plugins version changed ({cached_version} → {version}), re-downloading..."
|
||||
)
|
||||
else:
|
||||
log.info("WASM plugins found (unknown version, re-downloading...)")
|
||||
|
||||
os.makedirs(PLANO_PLUGINS_DIR, exist_ok=True)
|
||||
|
||||
for name, dest in [
|
||||
("prompt_gateway.wasm", prompt_gw_path),
|
||||
("llm_gateway.wasm", llm_gw_path),
|
||||
]:
|
||||
url = f"{PLANO_RELEASE_BASE_URL}/{version}/{name}"
|
||||
print(f"Downloading {name} ({version})...")
|
||||
print(f" URL: {url}")
|
||||
_download_file(url, dest)
|
||||
print(f" Saved to {dest}")
|
||||
|
||||
with open(version_path, "w") as f:
|
||||
f.write(version)
|
||||
|
||||
return prompt_gw_path, llm_gw_path
|
||||
|
||||
|
||||
def ensure_brightstaff_binary():
|
||||
"""Download brightstaff binary if not cached or version changed. Returns path to binary."""
|
||||
version = planoai.__version__
|
||||
brightstaff_path = os.path.join(PLANO_BIN_DIR, "brightstaff")
|
||||
version_path = os.path.join(PLANO_BIN_DIR, "brightstaff.version")
|
||||
|
||||
if os.path.exists(brightstaff_path) and os.access(brightstaff_path, os.X_OK):
|
||||
if os.path.exists(version_path):
|
||||
with open(version_path, "r") as f:
|
||||
cached_version = f.read().strip()
|
||||
if cached_version == version:
|
||||
log.info(f"brightstaff {version} found at {brightstaff_path}")
|
||||
return brightstaff_path
|
||||
print(
|
||||
f"brightstaff version changed ({cached_version} → {version}), re-downloading..."
|
||||
)
|
||||
else:
|
||||
log.info("brightstaff found (unknown version, re-downloading...)")
|
||||
|
||||
slug = _get_platform_slug()
|
||||
filename = f"brightstaff-{slug}"
|
||||
url = f"{PLANO_RELEASE_BASE_URL}/{version}/{filename}"
|
||||
|
||||
os.makedirs(PLANO_BIN_DIR, exist_ok=True)
|
||||
|
||||
print(f"Downloading brightstaff ({version}) for {slug}...")
|
||||
print(f" URL: {url}")
|
||||
_download_file(url, brightstaff_path)
|
||||
|
||||
os.chmod(brightstaff_path, 0o755)
|
||||
with open(version_path, "w") as f:
|
||||
f.write(version)
|
||||
print(f"brightstaff {version} installed at {brightstaff_path}")
|
||||
return brightstaff_path
|
||||
|
||||
|
||||
def find_wasm_plugins():
|
||||
"""Find WASM plugin files built from source. Returns (prompt_gateway_path, llm_gateway_path)."""
|
||||
repo_root = find_repo_root()
|
||||
|
|
|
|||
|
|
@ -13,15 +13,41 @@ from planoai.consts import (
|
|||
)
|
||||
from planoai.docker_cli import health_check_endpoint
|
||||
from planoai.native_binaries import (
|
||||
ensure_brightstaff_binary,
|
||||
ensure_envoy_binary,
|
||||
find_brightstaff_binary,
|
||||
find_wasm_plugins,
|
||||
ensure_wasm_plugins,
|
||||
)
|
||||
from planoai.utils import find_repo_root, getLogger
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
def _find_config_dir():
|
||||
"""Locate the directory containing plano_config_schema.yaml and envoy.template.yaml.
|
||||
|
||||
Checks package data first (pip-installed), then falls back to the repo checkout.
|
||||
"""
|
||||
import planoai
|
||||
|
||||
pkg_data = os.path.join(os.path.dirname(planoai.__file__), "data")
|
||||
if os.path.isdir(pkg_data) and os.path.exists(
|
||||
os.path.join(pkg_data, "plano_config_schema.yaml")
|
||||
):
|
||||
return pkg_data
|
||||
|
||||
repo_root = find_repo_root()
|
||||
if repo_root:
|
||||
config_dir = os.path.join(repo_root, "config")
|
||||
if os.path.isdir(config_dir):
|
||||
return config_dir
|
||||
|
||||
print(
|
||||
"Error: Could not find config templates. "
|
||||
"Make sure you're inside the plano repository or have the planoai package installed."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _temporary_env(overrides):
|
||||
"""Context manager that sets env vars from *overrides* and restores originals on exit."""
|
||||
|
|
@ -43,17 +69,9 @@ def render_native_config(plano_config_file, env, with_tracing=False):
|
|||
"""Render envoy and plano configs for native mode. Returns (envoy_config_path, plano_config_rendered_path)."""
|
||||
import yaml
|
||||
|
||||
repo_root = find_repo_root()
|
||||
if not repo_root:
|
||||
print(
|
||||
"Error: Could not find repository root. "
|
||||
"Make sure you're inside the plano repository."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
os.makedirs(PLANO_RUN_DIR, exist_ok=True)
|
||||
|
||||
prompt_gw_path, llm_gw_path = find_wasm_plugins()
|
||||
prompt_gw_path, llm_gw_path = ensure_wasm_plugins()
|
||||
|
||||
# If --with-tracing, inject tracing config if not already present
|
||||
effective_config_file = os.path.abspath(plano_config_file)
|
||||
|
|
@ -76,7 +94,7 @@ def render_native_config(plano_config_file, env, with_tracing=False):
|
|||
)
|
||||
|
||||
# Set environment variables that config_generator.validate_and_render_schema() reads
|
||||
config_dir = os.path.join(repo_root, "config")
|
||||
config_dir = _find_config_dir()
|
||||
overrides = {
|
||||
"PLANO_CONFIG_FILE": effective_config_file,
|
||||
"PLANO_CONFIG_SCHEMA_FILE": os.path.join(
|
||||
|
|
@ -150,8 +168,8 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
|
|||
print(msg)
|
||||
|
||||
envoy_path = ensure_envoy_binary()
|
||||
find_wasm_plugins() # validate they exist
|
||||
brightstaff_path = find_brightstaff_binary()
|
||||
ensure_wasm_plugins()
|
||||
brightstaff_path = ensure_brightstaff_binary()
|
||||
envoy_config_path, plano_config_rendered_path = render_native_config(
|
||||
plano_config_file, env, with_tracing=with_tracing
|
||||
)
|
||||
|
|
@ -267,7 +285,7 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
|
|||
stop_native()
|
||||
else:
|
||||
status_print(f"[dim]Logs: {log_dir}[/dim]")
|
||||
status_print(f"[dim]Run 'planoai down --native' to stop.[/dim]")
|
||||
status_print(f"[dim]Run 'planoai down' to stop.[/dim]")
|
||||
|
||||
|
||||
def _daemon_exec(args, env, log_path):
|
||||
|
|
@ -379,15 +397,7 @@ def stop_native():
|
|||
|
||||
def native_validate_config(plano_config_file):
|
||||
"""Validate config in-process without Docker."""
|
||||
repo_root = find_repo_root()
|
||||
if not repo_root:
|
||||
print(
|
||||
"Error: Could not find repository root. "
|
||||
"Make sure you're inside the plano repository."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
config_dir = os.path.join(repo_root, "config")
|
||||
config_dir = _find_config_dir()
|
||||
|
||||
# Create temp dir for rendered output (we just want validation)
|
||||
os.makedirs(PLANO_RUN_DIR, exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ path = "planoai/__init__.py"
|
|||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["planoai"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.force-include]
|
||||
"../config/plano_config_schema.yaml" = "planoai/data/plano_config_schema.yaml"
|
||||
"../config/envoy.template.yaml" = "planoai/data/envoy.template.yaml"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["planoai/**"]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue