mirror of
https://github.com/run-llama/workflows-py.git
synced 2026-07-19 08:46:21 -04:00
370 lines
10 KiB
Python
Executable File
370 lines
10 KiB
Python
Executable File
#!/usr/bin/env -S uv run --script
|
|
# /// script
|
|
# dependencies = ["click"]
|
|
# ///
|
|
"""
|
|
Local development environment for cloud_llama_deploy.
|
|
|
|
up — create/ensure cluster and start tilt
|
|
down — tear down tilt resources, retaining data
|
|
down --delete — also delete the kind cluster (kind target only)
|
|
|
|
Targets:
|
|
kind — (default) creates a kind cluster
|
|
docker-desktop — uses Docker Desktop's built-in Kubernetes
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from textwrap import dedent
|
|
|
|
import click
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent.absolute()
|
|
NAMESPACE = "llama-agents"
|
|
APPS_NAMESPACE = "llama-agents-apps"
|
|
|
|
TARGETS = ("kind", "docker-desktop")
|
|
K8S_CONTEXTS = {
|
|
"kind": "kind-kind",
|
|
"docker-desktop": "docker-desktop",
|
|
}
|
|
|
|
|
|
def run(
|
|
cmd: list[str], check: bool = True, capture: bool = False
|
|
) -> subprocess.CompletedProcess:
|
|
result = subprocess.run(
|
|
cmd, check=False, capture_output=capture, text=True, cwd=PROJECT_ROOT
|
|
)
|
|
if check and result.returncode != 0:
|
|
if capture and result.stderr:
|
|
print(result.stderr, file=sys.stderr)
|
|
sys.exit(1)
|
|
return result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# kind helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def kind_cluster_exists() -> bool:
|
|
result = run(["kind", "get", "clusters"], check=False, capture=True)
|
|
return "kind" in result.stdout
|
|
|
|
|
|
def ensure_kind_cluster() -> None:
|
|
if kind_cluster_exists():
|
|
result = run(
|
|
["kind", "export", "kubeconfig", "--name", "kind"],
|
|
check=False,
|
|
capture=True,
|
|
)
|
|
if result.returncode == 0:
|
|
return
|
|
print("Cluster 'kind' exists but is broken, recreating...")
|
|
run(["kind", "delete", "cluster", "--name", "kind"])
|
|
|
|
print("Creating kind cluster 'kind'...")
|
|
|
|
kind_config = dedent("""\
|
|
kind: Cluster
|
|
apiVersion: kind.x-k8s.io/v1alpha4
|
|
nodes:
|
|
- role: control-plane
|
|
kubeadmConfigPatches:
|
|
- |
|
|
kind: InitConfiguration
|
|
nodeRegistration:
|
|
kubeletExtraArgs:
|
|
node-labels: "ingress-ready=true"
|
|
extraPortMappings:
|
|
- containerPort: 80
|
|
hostPort: 8090
|
|
protocol: TCP
|
|
- containerPort: 443
|
|
hostPort: 8444
|
|
protocol: TCP
|
|
""")
|
|
|
|
import tempfile
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
|
f.write(kind_config)
|
|
config_path = f.name
|
|
|
|
try:
|
|
run(
|
|
[
|
|
"kind",
|
|
"create",
|
|
"cluster",
|
|
"--name",
|
|
"kind",
|
|
"--config",
|
|
config_path,
|
|
]
|
|
)
|
|
finally:
|
|
os.unlink(config_path)
|
|
|
|
install_ingress_controller("kind")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# docker-desktop helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def ensure_docker_desktop_cluster() -> None:
|
|
context = K8S_CONTEXTS["docker-desktop"]
|
|
result = run(
|
|
["kubectl", "--context", context, "cluster-info"],
|
|
check=False,
|
|
capture=True,
|
|
)
|
|
if result.returncode != 0:
|
|
print(
|
|
f"Kubernetes context '{context}' is not reachable.\n"
|
|
"Enable Kubernetes in Docker Desktop → Settings → Kubernetes.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
# Switch to the docker-desktop context
|
|
run(["kubectl", "config", "use-context", context])
|
|
install_ingress_controller("docker-desktop")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# shared helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
INGRESS_MANIFESTS = {
|
|
"kind": "https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml",
|
|
"docker-desktop": "https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml",
|
|
}
|
|
|
|
|
|
def install_ingress_controller(target: str) -> None:
|
|
result = run(
|
|
["kubectl", "get", "namespace", "ingress-nginx"], check=False, capture=True
|
|
)
|
|
if result.returncode == 0:
|
|
return
|
|
|
|
print("Installing nginx ingress controller...")
|
|
run(["kubectl", "apply", "-f", INGRESS_MANIFESTS[target]])
|
|
|
|
import time
|
|
|
|
start = time.time()
|
|
while time.time() - start < 10:
|
|
result = run(
|
|
[
|
|
"kubectl",
|
|
"get",
|
|
"pods",
|
|
"--namespace",
|
|
"ingress-nginx",
|
|
"--selector=app.kubernetes.io/component=controller",
|
|
"--no-headers",
|
|
],
|
|
check=False,
|
|
capture=True,
|
|
)
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
break
|
|
time.sleep(1)
|
|
|
|
run(
|
|
[
|
|
"kubectl",
|
|
"wait",
|
|
"--namespace",
|
|
"ingress-nginx",
|
|
"--for=condition=ready",
|
|
"pod",
|
|
"--selector=app.kubernetes.io/component=controller",
|
|
"--timeout=300s",
|
|
]
|
|
)
|
|
|
|
|
|
target_option = click.option(
|
|
"--target",
|
|
type=click.Choice(TARGETS),
|
|
default=None,
|
|
envvar="DEV_TARGET",
|
|
help="Kubernetes target cluster. [default: kind]",
|
|
)
|
|
|
|
apps_ns_option = click.option(
|
|
"--apps-namespace",
|
|
"apps_namespace",
|
|
is_flag=False,
|
|
flag_value=APPS_NAMESPACE,
|
|
default=None,
|
|
envvar="DEV_APPS_NAMESPACE",
|
|
help=(
|
|
"Run in split-namespace mode: put LlamaDeployment CRs and app "
|
|
f"resources in this namespace (default: {APPS_NAMESPACE}). "
|
|
"Unset = single-namespace mode."
|
|
),
|
|
)
|
|
|
|
|
|
def resolve_target(ctx: click.Context, target: str | None) -> str:
|
|
return target or ctx.obj.get("target") or "kind"
|
|
|
|
|
|
def resolve_apps_namespace(
|
|
ctx: click.Context, apps_namespace: str | None
|
|
) -> str | None:
|
|
return apps_namespace or ctx.obj.get("apps_namespace")
|
|
|
|
|
|
@click.group()
|
|
@target_option
|
|
@apps_ns_option
|
|
@click.pass_context
|
|
def cli(ctx: click.Context, target: str | None, apps_namespace: str | None) -> None:
|
|
"""Local development environment for cloud_llama_deploy."""
|
|
ctx.ensure_object(dict)
|
|
ctx.obj["target"] = target
|
|
ctx.obj["apps_namespace"] = apps_namespace
|
|
|
|
|
|
@cli.command()
|
|
@target_option
|
|
@apps_ns_option
|
|
@click.pass_context
|
|
def up(ctx: click.Context, target: str | None, apps_namespace: str | None) -> None:
|
|
"""Create/ensure cluster and start tilt."""
|
|
target = resolve_target(ctx, target)
|
|
apps_namespace = resolve_apps_namespace(ctx, apps_namespace)
|
|
|
|
# Check required tools
|
|
version_cmds: dict[str, list[str]] = {
|
|
"kubectl": ["kubectl", "version", "--client"],
|
|
"docker": ["docker", "--version"],
|
|
"tilt": ["tilt", "version"],
|
|
}
|
|
if target == "kind":
|
|
version_cmds["kind"] = ["kind", "--version"]
|
|
|
|
for tool, cmd in version_cmds.items():
|
|
if run(cmd, check=False, capture=True).returncode != 0:
|
|
print(f"Missing required tool: {tool}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if target == "kind":
|
|
ensure_kind_cluster()
|
|
else:
|
|
ensure_docker_desktop_cluster()
|
|
|
|
# Ensure namespaces exist
|
|
for ns in [NAMESPACE] + ([apps_namespace] if apps_namespace else []):
|
|
result = run(["kubectl", "get", "namespace", ns], check=False, capture=True)
|
|
if result.returncode != 0:
|
|
run(["kubectl", "create", "namespace", ns])
|
|
|
|
if not (PROJECT_ROOT / ".env").exists():
|
|
print(
|
|
"Note: no .env file found. GitHub integration requires GITHUB_APP_PRIVATE_KEY, GITHUB_APP_CLIENT_ID, GITHUB_APP_NAME, GITHUB_APP_SECRET."
|
|
)
|
|
|
|
ingress_port = "8090" if target == "kind" else "80"
|
|
print("Starting tilt...")
|
|
print(" API: http://localhost:8011")
|
|
print(" Tilt UI: http://localhost:10350")
|
|
print(f" Ingress: *.127.0.0.1.nip.io:{ingress_port}")
|
|
if apps_namespace:
|
|
print(f" Apps namespace: {apps_namespace}")
|
|
tilt_args = [
|
|
"tilt",
|
|
"up",
|
|
"-f",
|
|
str(PROJECT_ROOT / "operator" / "Tiltfile"),
|
|
"--",
|
|
target,
|
|
]
|
|
if apps_namespace:
|
|
tilt_args += ["--apps-namespace", apps_namespace]
|
|
os.execvp("tilt", tilt_args)
|
|
|
|
|
|
@cli.command()
|
|
@click.option("--delete", is_flag=True, help="Also delete the kind cluster")
|
|
@target_option
|
|
@apps_ns_option
|
|
@click.pass_context
|
|
def down(
|
|
ctx: click.Context,
|
|
delete: bool,
|
|
target: str | None,
|
|
apps_namespace: str | None,
|
|
) -> None:
|
|
"""Tear down tilt resources. Use --delete to also remove the cluster (kind only)."""
|
|
target = resolve_target(ctx, target)
|
|
apps_namespace = resolve_apps_namespace(ctx, apps_namespace)
|
|
|
|
tilt_args = [
|
|
"tilt",
|
|
"down",
|
|
"-f",
|
|
str(PROJECT_ROOT / "operator" / "Tiltfile"),
|
|
"--",
|
|
target,
|
|
]
|
|
if apps_namespace:
|
|
tilt_args += ["--apps-namespace", apps_namespace]
|
|
run(tilt_args, check=False)
|
|
|
|
if delete:
|
|
if target != "kind":
|
|
print("--delete only applies to the kind target", file=sys.stderr)
|
|
return
|
|
if kind_cluster_exists():
|
|
print("Deleting kind cluster 'kind'...")
|
|
run(["kind", "delete", "cluster", "--name", "kind"])
|
|
|
|
|
|
@cli.command()
|
|
@target_option
|
|
@apps_ns_option
|
|
@click.pass_context
|
|
def status(ctx: click.Context, target: str | None, apps_namespace: str | None) -> None:
|
|
"""Show cluster and deployment status."""
|
|
target = resolve_target(ctx, target)
|
|
apps_namespace = resolve_apps_namespace(ctx, apps_namespace)
|
|
context = K8S_CONTEXTS[target]
|
|
|
|
if target == "kind":
|
|
if not kind_cluster_exists():
|
|
print("No kind cluster 'kind' found")
|
|
return
|
|
run(["kind", "export", "kubeconfig", "--name", "kind"])
|
|
else:
|
|
result = run(
|
|
["kubectl", "--context", context, "cluster-info"],
|
|
check=False,
|
|
capture=True,
|
|
)
|
|
if result.returncode != 0:
|
|
print(f"Kubernetes context '{context}' is not reachable")
|
|
return
|
|
run(["kubectl", "config", "use-context", context])
|
|
|
|
run(["kubectl", "get", "pods", "-n", NAMESPACE], check=False)
|
|
if apps_namespace and apps_namespace != NAMESPACE:
|
|
print()
|
|
run(["kubectl", "get", "pods", "-n", apps_namespace], check=False)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|