#!/usr/bin/env bash set -e # Default values EVEREST_TOOL_BRANCH="main" # Script directory - where devrd is located SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # .devcontainer directory is always relative to the script location DEVCONTAINER_DIR="${SCRIPT_DIR}/../../.devcontainer" # .env file is always in the .devcontainer directory (relative to script) ENV_FILE="${DEVCONTAINER_DIR}/.env" # Function to load HOST_WORKSPACE_FOLDER from .env file # Usage: load_workspace_from_env [fallback] # If fallback is provided and workspace not found in .env, returns fallback # If no fallback provided, returns empty string (for use with ${var:-default} syntax) load_workspace_from_env() { local fallback="$1" if [ -f "$ENV_FILE" ]; then local workspace=$(grep "^HOST_WORKSPACE_FOLDER=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') if [ -n "$workspace" ]; then echo "$workspace" return fi fi # If fallback provided and workspace not found, return fallback if [ -n "$fallback" ]; then echo "$fallback" fi } # HOST_WORKSPACE_FOLDER is the folder that is mapped to /workspace in the container # Priority: 1) Command line/env var, 2) .env file, 3) Current directory HOST_WORKSPACE_FOLDER="${HOST_WORKSPACE_FOLDER:-$(load_workspace_from_env)}" HOST_WORKSPACE_FOLDER="${HOST_WORKSPACE_FOLDER:-$(pwd)}" # Docker Compose project name (defaults to workspace folder name with _devcontainer suffix, can be overridden) # This matches VSC's naming convention: {workspace-folder-name}_devcontainer-{service-name}-1 # If needed (and not running in VSCode), can be changed by setting the DOCKER_COMPOSE_PROJECT_NAME environment variable. DOCKER_COMPOSE_PROJECT_NAME="${DOCKER_COMPOSE_PROJECT_NAME:-$(basename "$HOST_WORKSPACE_FOLDER" | tr \"A-Z\" \"a-z\")_devcontainer}" # Function to detect if running inside container is_inside_container() { # Check for /.dockerenv (standard Docker indicator) [ -f /.dockerenv ] && return 0 # Check if /workspace exists and is mounted (devcontainer specific) [ -d /workspace ] && [ -f /workspace/.devcontainer/devrd ] && return 0 return 1 } # Function to show error when command is run from inside container show_inside_container_error() { local cmd_name="${1:-this command}" echo "✖ Error: This command cannot be run from inside the container" echo "" echo "You are currently inside the development container." echo "Please run this command from the host system instead:" echo "" echo " 1. Exit the container (type 'exit' or press Ctrl+D)" echo " 2. Run the command from your host terminal:" echo " ./devrd $cmd_name" echo "" exit 1 } # Function to run docker compose with static project name # Compose files are always relative to the script's .devcontainer directory docker_compose() { docker compose -p "$DOCKER_COMPOSE_PROJECT_NAME" \ -f "${DEVCONTAINER_DIR}/docker-compose.yml" \ -f "${DEVCONTAINER_DIR}/general-devcontainer/docker-compose.devcontainer.yml" "$@" } # Function to validate folder path validate_folder() { local folder="$1" # Convert relative path to absolute case "$folder" in /*) ;; # Already absolute *) folder="$(cd "$folder" && pwd)" ;; # Convert relative to absolute esac # Check if folder exists if [ ! -d "$folder" ]; then echo "Error: Folder '$folder' does not exist" exit 1 fi # Check if folder is readable if [ ! -r "$folder" ]; then echo "Error: Folder '$folder' is not accessible (permission denied)" exit 1 fi echo "$folder" } # Function to generate .env file generate_env() { if is_inside_container; then show_inside_container_error "env" fi # Process command line options if [ -n "$ENV_OPTIONS" ]; then set -- $ENV_OPTIONS while [ $# -gt 0 ]; do case "$1" in -v|--version) EVEREST_TOOL_BRANCH="$2" shift 2 ;; -w|--workspace) HOST_WORKSPACE_FOLDER="$2" shift 2 ;; *) shift ;; esac done fi # Set workspace folder if [ -n "$HOST_WORKSPACE_FOLDER" ]; then HOST_WORKSPACE_FOLDER=$(validate_folder "$HOST_WORKSPACE_FOLDER") else HOST_WORKSPACE_FOLDER="$(pwd)" fi # Get commit hash COMMIT_HASH=$(git ls-remote https://github.com/EVerest/everest-dev-environment.git ${EVEREST_TOOL_BRANCH} | cut -f1 2>/dev/null || echo "") # Check if we need to update existing file local needs_update=false if [ -f "$ENV_FILE" ] && [ -s "$ENV_FILE" ]; then # File exists, check if we have options that require updates if [ -n "$ENV_OPTIONS" ]; then needs_update=true fi fi if [ ! -f "$ENV_FILE" ] || [ ! -s "$ENV_FILE" ] || [ "$needs_update" = true ]; then cat > "$ENV_FILE" << EOF # Auto-generated by devrd script ORGANIZATION_ARG=EVerest REPOSITORY_HOST=github.com REPOSITORY_USER=git COMMIT_HASH=$COMMIT_HASH EVEREST_TOOL_BRANCH=$EVEREST_TOOL_BRANCH UID=$(id -u) GID=$(id -g) HOST_WORKSPACE_FOLDER=$HOST_WORKSPACE_FOLDER EOF if [ "$needs_update" = true ]; then echo "Updated .env file" else echo "Generated .env file" fi else echo "Found existing .env file" cat "$ENV_FILE" fi } # Function to build the container build_container() { if is_inside_container; then show_inside_container_error "build" fi echo "Building development container..." docker_compose --profile all build } # Function to get actual port mapping from docker compose get_port_mapping() { local service_name=$1 local internal_port=$2 # Get the actual port mapping from docker compose local port_mapping=$(docker_compose port $service_name $internal_port 2>/dev/null) if [ -n "$port_mapping" ]; then # Extract just the host port (remove the host part) echo "$port_mapping" | sed 's/.*://' else echo "" fi } # Function to display container links and tips display_container_status() { echo "" echo "Container Services Summary:" echo "==============================" # Get actual port mappings from docker compose local mqtt_explorer_port=$(get_port_mapping mqtt-explorer 4000) local steve_http_port=$(get_port_mapping steve 8180) # Display links with actual ports if [ -n "$mqtt_explorer_port" ]; then echo "MQTT Explorer: http://localhost:$mqtt_explorer_port" else echo "MQTT Explorer: currently not running" fi if [ -n "$steve_http_port" ]; then echo "Steve (HTTP): http://localhost:$steve_http_port" else echo "Steve (HTTP): currently not running" fi # Check if Node-RED is running if docker_compose ps | grep -q "nodered"; then echo "Node-RED UI: http://localhost:1880/ui" else echo "Node-RED UI: currently not running" fi echo "" echo "Tips:" echo " • MQTT Explorer: Browse and debug MQTT topics" echo " • Steve: OCPP backend management interface" echo " • Node-RED: Web-based UI for SIL simulations" echo " • Use './devrd prompt' to access the container shell" echo " • Use './devrd nodered-flows' to see available flows" echo "" } # Function to start containers using profiles start_compose_profile() { if is_inside_container; then show_inside_container_error "start" fi local profile_or_service="$1" if [ -n "$profile_or_service" ]; then echo "Starting containers for profile/service: $profile_or_service..." docker_compose --profile "$profile_or_service" up -d else echo "Starting the development container and all services..." docker_compose --profile all up -d fi # Display workspace mapping echo "Workspace mapping: $HOST_WORKSPACE_FOLDER → /workspace" echo "" # Display container links display_container_status } # Function to stop containers using profiles or container name pattern stop_compose_profile() { if is_inside_container; then show_inside_container_error "stop" fi local profile_or_pattern="$1" if [ -n "$profile_or_pattern" ]; then # Check if it's a valid profile name case "$profile_or_pattern" in mqtt|ocpp|sil|all) echo "Stopping containers for profile: $profile_or_pattern..." docker_compose --profile "$profile_or_pattern" stop ;; *) # Treat as container name pattern echo "Stopping containers matching pattern: $profile_or_pattern..." local containers=$(docker ps --format "{{.Names}}" | grep -E "($profile_or_pattern)" || true) if [ -z "$containers" ]; then echo "No running containers found matching pattern: $profile_or_pattern" return 1 fi echo "$containers" | while read container; do echo "Stopping container: $container" docker stop "$container" 2>/dev/null || echo "Failed to stop container: $container" done ;; esac else echo "Stopping the development container and all services..." docker_compose --profile all stop fi } # Function to purge everything purge_everything() { if is_inside_container; then show_inside_container_error "purge" fi local purge_pattern="${1:-$(basename "$HOST_WORKSPACE_FOLDER")}" local current_project="$(basename "$HOST_WORKSPACE_FOLDER")" echo "Purging all devcontainer resources for pattern: $purge_pattern..." # Only use docker_compose down if purging the current project if [ "$purge_pattern" = "$current_project" ]; then echo "Stopping and removing containers for current project..." docker_compose down -v --remove-orphans else echo "Purging resources for different project pattern: $purge_pattern" echo "Skipping docker-compose cleanup (not current project)" fi # Remove all images related to the project echo "Removing devcontainer images..." docker images --format "table {{.Repository}}:{{.Tag}}" | grep -E "($purge_pattern)" | awk '{print $1}' | xargs -r docker rmi -f # Remove all volumes related to the project (with force if needed) echo "Removing devcontainer volumes..." docker volume ls --format "{{.Name}}" | grep -E "($purge_pattern)" | while read volume; do echo "Removing volume: $volume" docker volume rm -f "$volume" 2>/dev/null || echo "Volume $volume could not be removed (may be in use)" done # Ask user if they want to purge CPM cache volume echo "" echo "CPM source cache volume (everest-cpm-source-cache) is shared across all workspaces." read -p "Do you want to purge the CPM cache volume as well? [y/N]: " purge_cache purge_cache="${purge_cache:-N}" if [[ "$purge_cache" =~ ^[Yy]$ ]]; then echo "Removing CPM cache volume..." if docker volume rm everest-cpm-source-cache 2>/dev/null; then echo "✔ CPM cache volume removed" else echo "⚠ CPM cache volume could not be removed (may be in use or not exist)" fi else echo "Keeping CPM cache volume (will be reused for faster builds)" fi # Remove any dangling images and containers echo "" echo "Cleaning up dangling resources..." docker system prune -f echo "" echo "✔ Purge complete! All devcontainer resources have been removed." } # Function to check if SSH agent is running check_ssh_agent() { if [ -z "$SSH_AUTH_SOCK" ] || ! ssh-add -l >/dev/null 2>&1; then echo "Error: SSH agent is not running or no keys are loaded." echo "Please start the SSH agent and add your keys:" echo " eval \$(ssh-agent)" echo " ssh-add ~/.ssh/id_rsa # or your private key" echo "Or if you're using a different key:" echo " ssh-add ~/.ssh/your_private_key" exit 1 fi } # Function to execute a command in the container exec_devcontainer() { if is_inside_container; then echo "✖ You're already inside the container." echo "" echo "To run a command, just execute it directly:" if [ $# -gt 0 ]; then echo " $@" else echo " " fi exit 1 fi echo "Checking if development container is running..." # Check if the devcontainer service is running if ! docker_compose ps devcontainer | grep -q "Up"; then echo "Error: Development container is not running." echo "Please start the container first with: ./devrd start" echo "Or build and start with: ./devrd build && ./devrd start" exit 1 fi echo "Executing command in development container..." run_in_devcontainer "$@" } # Function to get a shell prompt in the container prompt_devcontainer() { if is_inside_container; then echo "✖ You're already inside the container shell." exit 1 fi echo "Starting shell in development container..." exec_devcontainer /bin/bash } # Helper function to check if Node-RED is running and get the URL # Sets nodered_url variable and returns 0 if running, 1 if not check_nodered_running() { if is_inside_container; then nodered_url="http://nodered:1880" curl -s "$nodered_url/flows" >/dev/null 2>&1 && return 0 else nodered_url="http://localhost:1880" docker_compose ps | grep -q "nodered" && return 0 fi return 1 } # Helper function to execute a command in the container # Usage: run_in_devcontainer [--no-tty] [args...] # Executes directly if inside container, via docker_compose exec if on host # No error checking - assumes container is running when called from host # Use --no-tty for non-interactive commands that need output capture run_in_devcontainer() { local no_tty=false if [ "$1" = "--no-tty" ]; then no_tty=true shift fi if is_inside_container; then "$@" else if [ "$no_tty" = true ]; then docker_compose exec -T devcontainer "$@" else docker_compose exec devcontainer "$@" fi fi } # Function to list available flows list_nodered_flows() { echo "" echo "Available Node-RED Flows:" echo "=============================" # Check if Node-RED is running if ! check_nodered_running; then echo "✖ Node-RED container is not running" echo "Please start with './devrd start' first" return 1 fi # Find all flow files in the workspace local flows if is_inside_container; then flows=$(find /workspace -name "*-flow.json" -type f 2>/dev/null | sort) else flows=$(docker_compose exec -T devcontainer find /workspace -name "*-flow.json" -type f 2>/dev/null | sort) fi if [ -z "$flows" ]; then echo "No flow files found in workspace" echo "" echo "Expected pattern: *-flow.json" echo "Search location: /workspace" return 1 fi echo "Found $(echo "$flows" | wc -l) flow file(s):" echo "" for flow in $flows; do # Remove /workspace/ prefix to get relative path from workspace root local relative_path=$(echo "$flow" | sed 's|^/workspace/||') echo " Path: $relative_path" done echo "" echo "Usage: ./devrd flow " echo "Example: ./devrd flow EVerest/config/nodered/config-sil-dc-flow.json" echo "" } # Function to switch flow using REST API switch_nodered_flow() { local flow_path="$1" if [ -z "$flow_path" ]; then echo "Error: Please specify a flow file path" echo "" echo "Available flows:" list_nodered_flows return 1 fi # Check if Node-RED is running if ! check_nodered_running; then echo "✖ Node-RED container is not running" echo "Please start with './devrd start' first" return 1 fi # Construct full path in container local full_path="/workspace/$flow_path" # Check if file exists and is readable, then copy to temp file if ! run_in_devcontainer --no-tty test -r "$full_path"; then echo "✖ Flow file not found or not readable: $flow_path" echo "" echo "Available flows:" list_nodered_flows return 1 fi # Copy flow to temporary file run_in_devcontainer --no-tty cat "$full_path" > /tmp/flows.json echo "Switching Node-RED to flow: $(basename "$flow_path")" echo "Source: $flow_path" # Process environment variables in the flow JSON # Replace "broker": "localhost" with "broker": "mqtt-server" sed -i 's/"broker": "localhost"/"broker": "mqtt-server"/g' /tmp/flows.json # Deploy flow via Node-RED REST API echo "Deploying flow via Node-RED API..." local response=$(curl -s -w "%{http_code}" -X POST "$nodered_url/flows" \ -H "Content-Type: application/json" \ -d @/tmp/flows.json) local http_code="${response: -3}" if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then echo "✔ Node-RED flow deployed successfully via API!" if is_inside_container; then echo "Access at: http://nodered:1880/ui (from container) or http://localhost:1880/ui (from host)" else echo "Access at: http://localhost:1880/ui" fi else echo "✖ Failed to deploy flow via API (HTTP $http_code)" echo "Response: ${response%???}" return 1 fi # Clean up temporary file rm -f /tmp/flows.json } # Function to display help show_help() { echo "Usage: $0 [COMMAND] [OPTIONS]" echo "" echo "Commands:" echo " env Generate .env file with repository information (default)" echo " build Build the development container" echo " start [profile] Start containers (profiles: mqtt, ocpp, sil, all)" echo " stop [profile|pattern] Stop containers by profile (mqtt, ocpp, sil, all) or container name pattern" echo " purge [pattern] Remove all devcontainer resources (containers, images, volumes)" echo " Optional pattern to match (default: current folder name)" echo " exec Execute a command in the development container (requires the container to be running)" echo " prompt Get a shell prompt in the development container (requires the container to be running)" echo " flows List available flows" echo " flow Switch to specific flow file" echo "" echo "Options (for env command only):" echo " -v, --version VERSION Everest tool branch (default: $EVEREST_TOOL_BRANCH, preserves existing if not specified)" echo " -w, --workspace DIR Workspace directory to map to /workspace in container (default: current directory)" echo " --help Display this help message" echo "" echo "Examples:" echo " $0 env # Generate .env file with repository information" echo " $0 build # Build container" echo " $0 start # Start all containers" echo " $0 start sil # Start SIL simulation tools (Node-RED, MQTT Explorer)" echo " $0 start ocpp # Start OCPP services (Steve, OCPP DB, MQTT)" echo " $0 start mqtt # Start only MQTT server" echo " $0 stop sil # Stop SIL simulation tools" echo " $0 stop ev-ws # Stop all containers matching pattern 'ev-ws'" echo " $0 purge # Remove all devcontainer resources for current folder" echo " $0 purge my-project # Remove all devcontainer resources matching 'my-project'" echo " $0 exec ls -la # Execute command in container" echo " $0 prompt # Get shell prompt in container" echo " $0 flows # List available flows" echo " $0 flow # Switch to specific Node-RED flow file" echo " $0 -w ~/Documents # Map Documents folder to /workspace" echo " $0 --workspace /opt/tools # Map tools folder to /workspace" exit 0 } # Parse command line arguments COMMAND="env" ENV_OPTIONS="" # First pass: collect all options while [ $# -gt 0 ]; do case $1 in -v|--version|-w|--workspace) # Store env-specific options for later use ENV_OPTIONS="$ENV_OPTIONS $1 $2" shift 2 ;; --help) show_help ;; exec) COMMAND="$1" shift # For exec, pass all remaining arguments to the exec function break ;; env|build|prompt|flows) COMMAND="$1" shift # Don't break here, continue to collect more options ;; flow) COMMAND="$1" shift # For flow, pass any remaining arguments as flow path break ;; purge) COMMAND="$1" shift # For purge, pass any remaining arguments as pattern break ;; start|stop) COMMAND="$1" shift # For start/stop, pass any remaining arguments as container name break ;; *) echo "Unknown option: $1" show_help ;; esac done # Execute the command case $COMMAND in env) # Check SSH agent for Git operations check_ssh_agent generate_env ;; build) # Only generate env if .env file doesn't exist or is empty if [ ! -f "$ENV_FILE" ] || [ ! -s "$ENV_FILE" ]; then # Check SSH agent for Git operations check_ssh_agent generate_env fi build_container ;; start) # Only generate env if .env file doesn't exist or is empty if [ ! -f "$ENV_FILE" ] || [ ! -s "$ENV_FILE" ]; then # Check SSH agent for Git operations check_ssh_agent generate_env fi start_compose_profile "$@" ;; stop) stop_compose_profile "$@" ;; purge) purge_everything "$@" ;; exec) if [ $# -eq 0 ]; then echo "Error: exec command requires arguments" show_help fi exec_devcontainer "$@" ;; prompt) prompt_devcontainer ;; flows) list_nodered_flows ;; flow) if [ $# -eq 0 ]; then echo "Error: flow command requires a flow file path" show_help fi switch_nodered_flow "$1" ;; *) echo "Unknown command: $COMMAND" show_help ;; esac