Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ backend/bin/
.idea/
.vscode/

# Dev environment state (generated certs, runtime artifacts)
# Dev environment state (generated certs, runtime artifacts, env config)
scripts/.pki/
scripts/.state/
scripts/.env.dev

# Env files are never committed
.env
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ PROTO_GRPC_OPTS := \
--go-grpc_opt=Minference.proto=$(GO_MODULE)/gen/inferencev1 \
--go-grpc_opt=Mopenshell.proto=$(GO_MODULE)/gen/openshellv1

# Auto-source dev environment config if available (written by scripts/dev-env.sh)
-include scripts/.env.dev
export

.PHONY: setup proto dev dev-full dev-backend dev-frontend build build-frontend build-backend test lint typecheck clean

setup: ## Install frontend deps and Go deps
Expand All @@ -32,15 +36,15 @@ proto: ## Regenerate Go stubs from backend/proto/*.proto into backend/gen/
$(addprefix $(PROTO_DIR)/,$(PROTO_FILES))
cd backend && go mod tidy

dev-full: ## Start dev infrastructure (Keycloak + gateway) then frontend + BFF
dev-full: ## Start Keycloak + gateway, then frontend + BFF (one command)
./scripts/dev-env.sh start
@$(MAKE) dev

dev: ## Start frontend dev server (:3000) and Go BFF (:8080)
@$(MAKE) -j2 dev-backend dev-frontend

dev-backend:
cd backend && AUTH_DISABLED=$${AUTH_DISABLED:-true} go run ./cmd/server
cd backend && go run ./cmd/server

dev-frontend:
cd frontend && npm start
Expand Down
35 changes: 21 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,13 @@ To test with real OIDC authentication against a local Keycloak and OpenShell gat

```bash
make setup
export OPENSHELL_DIR=~/path/to/openshell # your OpenShell checkout
make dev-full # starts infra + dashboard
```

# Point at your OpenShell source checkout
export OPENSHELL_DIR=~/path/to/openshell
That's it. `dev-full` starts Keycloak and the gateway (if not already running), writes a `scripts/.env.dev` config file, and launches the dashboard. On subsequent runs, `make dev` picks up the config automatically (no env vars needed).

# Start the infrastructure (Keycloak + gateway)
./scripts/dev-env.sh start

# Run the dashboard with the printed env vars
export OPENSHELL_GATEWAY_URL=grpcs://localhost:17670
export OIDC_ISSUER=http://localhost:8180/realms/openshell
export OIDC_CLIENT_ID=openshell-dashboard
export GATEWAY_CA_CERT=$(pwd)/scripts/.pki/ca.crt
export AUTH_DISABLED=false
make dev
```
If `OPENSHELL_DIR` is not set, the script prompts interactively and offers to clone the repo for you. The chosen path is saved to `scripts/.env.dev` so you only configure it once.

Open http://localhost:3000 and log in via Keycloak with one of the test users:

Expand All @@ -60,7 +52,22 @@ Open http://localhost:3000 and log in via Keycloak with one of the test users:
| `user@test` | `user` | Workspace member |
| `user-b@test` | `user-b` | Workspace member |

The script is idempotent. Run `./scripts/dev-env.sh status` to check components, `stop` to tear down, or `rebuild-gateway` after pulling upstream changes.
### What `dev-full` starts

| Component | How | Lifecycle |
|-----------|-----|-----------|
| Keycloak | Podman container (`openshell-keycloak`) on port 8180 | Runs until `dev-env.sh stop` |
| OpenShell gateway | Background process built from source, port 17670 (gRPCs) + 17671 (health) | Runs until `dev-env.sh stop` |
| Dashboard BFF | `go run` on port 8080 | Runs with `make dev`, Ctrl+C to stop |
| Dashboard frontend | Webpack dev server on port 3000 | Runs with `make dev`, Ctrl+C to stop |

Keycloak and the gateway survive across `make dev` restarts. Stop them explicitly:

```bash
./scripts/dev-env.sh stop # stops gateway + keycloak, cleans up orphans
./scripts/dev-env.sh status # check what's running
./scripts/dev-env.sh rebuild-gateway # rebuild after upstream changes
```

## Configuration

Expand Down
163 changes: 141 additions & 22 deletions scripts/dev-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,65 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PKI_DIR="$SCRIPT_DIR/.pki"
STATE_DIR="$SCRIPT_DIR/.state"
if [ -z "${OPENSHELL_DIR:-}" ]; then
echo "ERROR: OPENSHELL_DIR is not set." >&2
echo "Set it to the path of your OpenShell source checkout:" >&2
echo " export OPENSHELL_DIR=~/path/to/openshell" >&2
exit 1
fi
OPENSHELL_DIR="${OPENSHELL_DIR}"
ENV_FILE="$SCRIPT_DIR/.env.dev"

resolve_openshell_dir() {
if [ -n "${OPENSHELL_DIR:-}" ]; then
return 0
fi

if [ -f "$ENV_FILE" ]; then
local saved
saved=$(grep '^OPENSHELL_DIR=' "$ENV_FILE" 2>/dev/null | cut -d= -f2-)
if [ -n "$saved" ] && [ -d "$saved" ]; then
OPENSHELL_DIR="$saved"
return 0
fi
fi

echo ""
echo "OpenShell source directory not configured."
echo ""
echo "Where is your OpenShell checkout?"
echo ""
echo " 1) Enter a path"
echo " 2) Clone from GitHub into ./openshell (next to this project)"
echo ""
printf "Choice [1/2]: "
read -r choice

case "$choice" in
2)
local clone_dir="$PROJECT_DIR/../openshell"
if [ -d "$clone_dir" ] && [ -f "$clone_dir/Cargo.toml" ]; then
echo "Found existing checkout at $clone_dir"
OPENSHELL_DIR="$(cd "$clone_dir" && pwd)"
else
echo "Cloning NVIDIA/OpenShell..."
git clone https://github.com/NVIDIA/OpenShell.git "$clone_dir" 2>&1
OPENSHELL_DIR="$(cd "$clone_dir" && pwd)"
fi
;;
*)
printf "Path to OpenShell source: "
read -r user_path
user_path="${user_path/#\~/$HOME}"
if [ ! -d "$user_path" ] || [ ! -f "$user_path/Cargo.toml" ]; then
error "Not a valid OpenShell checkout: $user_path"
exit 1
fi
OPENSHELL_DIR="$(cd "$user_path" && pwd)"
;;
esac

echo "OPENSHELL_DIR=$OPENSHELL_DIR" >> "$ENV_FILE" 2>/dev/null || true
export OPENSHELL_DIR
}

OPENSHELL_DIR="${OPENSHELL_DIR:-}"
KEYCLOAK_PORT="${KEYCLOAK_PORT:-8180}"
KEYCLOAK_CONTAINER="openshell-keycloak"
PODMAN_NETWORK="openshell-dev"
GATEWAY_GRPC_PORT=17670
GATEWAY_HTTP_PORT=17671
GATEWAY_PID_FILE="$STATE_DIR/gateway.pid"
Expand Down Expand Up @@ -128,7 +178,7 @@ generate_pki() {
-subj "/CN=localhost" 2>/dev/null
cat > "$PKI_DIR/server/san.cnf" <<EOF
[v3_req]
subjectAltName = DNS:localhost, IP:127.0.0.1
subjectAltName = DNS:localhost, DNS:host.containers.internal, IP:127.0.0.1
EOF
openssl x509 -req -in "$PKI_DIR/server/tls.csr" \
-CA "$PKI_DIR/ca.crt" -CAkey "$PKI_DIR/ca.key" -CAcreateserial \
Expand Down Expand Up @@ -378,11 +428,29 @@ build_gateway() {
info "openshell-gateway built"
}

ensure_podman_network() {
if podman network inspect "$PODMAN_NETWORK" >/dev/null 2>&1; then
return 0
fi
podman network create --driver bridge "$PODMAN_NETWORK" >/dev/null 2>&1
info "Podman network '$PODMAN_NETWORK' created"
}

generate_gateway_config() {
mkdir -p "$STATE_DIR"
local podman_socket
podman_socket=$(detect_podman_socket)

ensure_podman_network

local jwt_dir="$STATE_DIR/jwt"
if [ ! -f "$jwt_dir/signing.pem" ]; then
mkdir -p "$jwt_dir"
(umask 077; openssl genpkey -algorithm Ed25519 -out "$jwt_dir/signing.pem" 2>/dev/null)
openssl pkey -in "$jwt_dir/signing.pem" -pubout -out "$jwt_dir/public.pem" 2>/dev/null
openssl rand -hex 16 > "$jwt_dir/kid"
fi

cat > "$GATEWAY_CONFIG_FILE" <<EOF
[openshell]
version = 1
Expand All @@ -391,8 +459,14 @@ version = 1
bind_address = "0.0.0.0:${GATEWAY_GRPC_PORT}"
compute_drivers = ["podman"]

[openshell.gateway.gateway_jwt]
signing_key_path = "$jwt_dir/signing.pem"
public_key_path = "$jwt_dir/public.pem"
kid_path = "$jwt_dir/kid"

[openshell.drivers.podman]
socket_path = "$podman_socket"
network_name = "$PODMAN_NETWORK"
gateway_port = ${GATEWAY_GRPC_PORT}
guest_tls_ca = "$PKI_DIR/ca.crt"
guest_tls_cert = "$PKI_DIR/client/tls.crt"
Expand Down Expand Up @@ -491,8 +565,10 @@ create_default_workspace() {

if command -v grpcurl &>/dev/null; then
local result
local proto_import="$OPENSHELL_DIR/proto"
result=$(grpcurl -H "Authorization: Bearer $admin_token" \
-cacert "$PKI_DIR/ca.crt" \
-import-path "$proto_import" -proto openshell.proto \
-d '{"name": "default"}' \
"localhost:${GATEWAY_GRPC_PORT}" \
openshell.v1.OpenShell/CreateWorkspace 2>&1 || true)
Expand All @@ -517,19 +593,36 @@ create_default_workspace() {
fi
}

write_env_file() {
cat > "$ENV_FILE" <<EOF
# Generated by dev-env.sh — do not edit manually
OPENSHELL_DIR=${OPENSHELL_DIR}
OPENSHELL_GATEWAY_URL=localhost:${GATEWAY_GRPC_PORT}
OIDC_ISSUER=http://localhost:${KEYCLOAK_PORT}/realms/openshell
OIDC_CLIENT_ID=openshell-dashboard
GATEWAY_CA_CERT=${PKI_DIR}/ca.crt
AUTH_DISABLED=false
EOF
}

print_env_vars() {
write_env_file
echo ""
echo "Ready! Run the dashboard with:"
echo ""
echo " export OPENSHELL_GATEWAY_URL=grpcs://localhost:${GATEWAY_GRPC_PORT}"
echo " export OIDC_ISSUER=http://localhost:${KEYCLOAK_PORT}/realms/openshell"
echo " export OIDC_CLIENT_ID=openshell-dashboard"
echo " export GATEWAY_CA_CERT=${PKI_DIR}/ca.crt"
echo " make dev"
echo " make setup # first time only"
echo " make dev # auto-reads scripts/.env.dev"
echo ""
echo "Or for the full stack in one command:"
echo ""
echo " make dev-full"
echo ""
echo "Then open http://localhost:3000 and log in with admin@test / admin"
echo ""
}

cmd_start() {
resolve_openshell_dir
check_prerequisites

local port_failed=0
Expand Down Expand Up @@ -557,25 +650,50 @@ cmd_start() {
cmd_stop() {
step "Stopping dev environment"

if gateway_is_running; then
# Gateway: try PID file first, then scan for orphaned processes
if [ -f "$GATEWAY_PID_FILE" ]; then
local pid
pid=$(cat "$GATEWAY_PID_FILE")
kill "$pid" 2>/dev/null || true
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
local waited=0
while kill -0 "$pid" 2>/dev/null && [ "$waited" -lt 10 ]; do
sleep 1
waited=$((waited + 1))
done
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
warn "Gateway killed forcefully (PID $pid)"
else
info "Gateway stopped (PID $pid)"
fi
else
info "Gateway PID $pid already gone (stale PID file)"
fi
rm -f "$GATEWAY_PID_FILE"
info "Gateway stopped (PID $pid)"
else
info "Gateway not running"
fi

if keycloak_is_running; then
podman stop "$KEYCLOAK_CONTAINER" >/dev/null 2>&1 || true
podman rm "$KEYCLOAK_CONTAINER" >/dev/null 2>&1 || true
# Check for orphaned gateway processes on the expected port
local orphan_pid
orphan_pid=$(lsof -ti :"$GATEWAY_GRPC_PORT" 2>/dev/null || true)
if [ -n "$orphan_pid" ]; then
kill "$orphan_pid" 2>/dev/null || true
warn "Killed orphaned process on port $GATEWAY_GRPC_PORT (PID $orphan_pid)"
fi

# Keycloak: force remove regardless of state (handles stopped, running, or broken containers)
if podman container exists "$KEYCLOAK_CONTAINER" 2>/dev/null; then
podman stop "$KEYCLOAK_CONTAINER" 2>/dev/null || true
podman rm -f "$KEYCLOAK_CONTAINER" 2>/dev/null || true
info "Keycloak stopped and removed"
else
podman rm -f "$KEYCLOAK_CONTAINER" >/dev/null 2>&1 || true
info "Keycloak not running"
fi

# Clean up stale state files (preserve PKI, DB, and env config)
rm -f "$GATEWAY_PID_FILE" "$GATEWAY_LOG_FILE" "$GATEWAY_CONFIG_FILE"
info "State files cleaned"

echo ""
}

Expand Down Expand Up @@ -608,6 +726,7 @@ cmd_status() {
}

cmd_rebuild_gateway() {
resolve_openshell_dir
step "Rebuilding gateway"

if gateway_is_running; then
Expand Down