homelab infrastructure services
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ports): Implement coordinated port allocation for multi-service deployments

- Add port allocation functions to lib/metadata.sh:
- get_service_port_count(): Parse docker-compose.yml for port needs
- find_available_port_range(): First-fit allocation algorithm
- allocate_service_ports(): Central port allocation with registry
- deallocate_service_ports(): Free ports on service removal
- generate_service_env(): Create service .env with TIN_PORT_* vars

- Update cmd/service/deploy.sh:
- Allocate ports before service deployment
- Generate per-service .env files
- Show allocated port range in success message
- Clean up on allocation failure

- Update cmd/service/rm.sh:
- Deallocate ports on service removal
- Update success message to note freed ports

- Remove old allocate_service_ports() from lib/core.sh
- Old implementation did not support multi-service coordination

- Fix .gitignore: Change service/ to /service/ to allow cmd/service/ tracking

- Add cmd/service/*.sh files (previously untracked)
- These CLI commands are part of the platform

- Update OODA documentation:
- Add ACT-3 plan.md with implementation details
- Update outcomes.md with port-allocation outcome and tests
- Update BRANCH.md to track progress

Refs: ooda/2025-10-multi-service-architecture/orient/port-allocation-algorithm.md
Refs: ooda/2025-10-multi-service-architecture/act/03-port-allocation/plan.md

+1455 -58
+227
cmd/service/deploy.sh
··· 1 + #!/bin/bash 2 + # tin service deploy - Deploy service from catalog 3 + 4 + set -euo pipefail 5 + 6 + # Get tinsnip root and source libraries 7 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 + TINSNIP_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" 9 + source "$TINSNIP_ROOT/lib/core.sh" 10 + source "$TINSNIP_ROOT/lib/uid.sh" 11 + source "$TINSNIP_ROOT/lib/metadata.sh" 12 + 13 + # Helper function to calculate port numbers 14 + get_service_port() { 15 + local service_name="$1" 16 + local service_env="$2" 17 + local port_index="${3:-0}" 18 + 19 + local service_uid=$(calculate_service_uid "$service_name" "$service_env") 20 + echo $((service_uid + port_index)) 21 + } 22 + 23 + # Deploy service from catalog 24 + deploy_service() { 25 + local service_env="$1" 26 + local catalog_service="$2" 27 + 28 + # Parse service-environment 29 + local parsed_output 30 + if ! parsed_output=$(parse_machine_name "$service_env" 2>/dev/null); then 31 + error_with_prefix "Service Deploy" "Invalid machine environment format: '$service_env'" 32 + echo "Expected: <service>-<environment> (e.g., gazette-prod, bsky-pds-dev)" >&2 33 + exit 1 34 + fi 35 + 36 + local machine_service=$(echo "$parsed_output" | sed -n '1p') 37 + local environment=$(echo "$parsed_output" | sed -n '2p') 38 + local service_user="$service_env" 39 + 40 + log_with_prefix "Service Deploy" "Deploying service: $catalog_service → $service_env" 41 + echo 42 + 43 + # Debug: Show current TIN_SHEET 44 + log_with_prefix "Service Deploy" "Using sheet: ${TIN_SHEET:-topsheet}" 45 + 46 + # Verify machine environment exists 47 + local service_uid 48 + if ! service_uid=$(calculate_service_uid "$machine_service" "$environment" 2>/dev/null); then 49 + error_with_prefix "Service Deploy" "Cannot calculate UID for machine environment" 50 + exit 1 51 + fi 52 + 53 + log_with_prefix "Service Deploy" "Calculated UID: $service_uid" 54 + 55 + if ! getent passwd "$service_uid" >/dev/null 2>&1; then 56 + error_with_prefix "Service Deploy" "Machine environment '$service_env' not found" 57 + echo "Create with: tin machine create $machine_service $environment" >&2 58 + exit 1 59 + fi 60 + 61 + # Check if service catalog exists 62 + local service_catalog_path="$HOME/.local/opt/dynamicalsystem.service" 63 + if [[ ! -d "$service_catalog_path/$catalog_service" ]]; then 64 + error_with_prefix "Service Deploy" "Service '$catalog_service' not found in catalog" 65 + echo "Available services:" >&2 66 + if [[ -d "$service_catalog_path" ]]; then 67 + ls -1 "$service_catalog_path" | grep -v README | head -10 >&2 68 + fi 69 + exit 1 70 + fi 71 + 72 + log_with_prefix "Service Deploy" "Machine environment verified: $service_env" 73 + log_with_prefix "Service Deploy" "Service catalog found: $catalog_service" 74 + 75 + # Show deployment plan 76 + echo "Deployment Plan:" 77 + echo " Machine: $service_env" 78 + echo " Service: $catalog_service" 79 + echo " User: $service_user" 80 + echo " UID: $service_uid" 81 + echo " Source: $service_catalog_path/$catalog_service" 82 + echo " Target: /mnt/$service_env/service/$catalog_service" 83 + echo 84 + 85 + # Confirm deployment 86 + read -p "Deploy $catalog_service to $service_env? [Y/n]: " confirm 87 + case "${confirm:-y}" in 88 + [Yy]*|"") 89 + log_with_prefix "Service Deploy" "Starting deployment..." 90 + ;; 91 + [Nn]*) 92 + log_with_prefix "Service Deploy" "Deployment cancelled" 93 + exit 1 94 + ;; 95 + *) 96 + error_with_prefix "Service Deploy" "Invalid input. Deployment cancelled" 97 + exit 1 98 + ;; 99 + esac 100 + 101 + # Copy service files 102 + log_with_prefix "Service Deploy" "Copying service files..." 103 + sudo -u "$service_user" mkdir -p "/mnt/$service_env/service" 104 + 105 + # Copy as current user (to avoid permission issues reading from home directory) 106 + # then fix ownership for the service user 107 + if cp -r "$service_catalog_path/$catalog_service" "/mnt/$service_env/service/"; then 108 + # Fix ownership to service user (NFS all_squash will handle actual ownership) 109 + sudo chown -R "$service_uid:$service_uid" "/mnt/$service_env/service/$catalog_service" 2>/dev/null || true 110 + log_with_prefix "Service Deploy" "Service files copied successfully" 111 + else 112 + error_with_prefix "Service Deploy" "Failed to copy service files" 113 + exit 1 114 + fi 115 + 116 + # Allocate ports and generate service .env 117 + log_with_prefix "Service Deploy" "Allocating ports..." 118 + local compose_file="/mnt/$service_env/service/$catalog_service/docker-compose.yml" 119 + local port_count 120 + port_count=$(get_service_port_count "$compose_file") 121 + 122 + local start_port 123 + if ! start_port=$(allocate_service_ports "$machine_service" "$environment" "$catalog_service" "$port_count"); then 124 + error_with_prefix "Service Deploy" "Port allocation failed" 125 + # Clean up copied files 126 + sudo rm -rf "/mnt/$service_env/service/$catalog_service" 127 + exit 1 128 + fi 129 + 130 + if ! generate_service_env "$service_env" "$catalog_service" "$start_port" "$port_count"; then 131 + error_with_prefix "Service Deploy" "Failed to generate service .env" 132 + # Clean up 133 + sudo rm -rf "/mnt/$service_env/service/$catalog_service" 134 + exit 1 135 + fi 136 + 137 + local end_port=$((start_port + port_count - 1)) 138 + log_with_prefix "Service Deploy" "✓ Ports allocated: $start_port-$end_port ($port_count ports)" 139 + 140 + # Run service setup if it exists 141 + local setup_script="/mnt/$service_env/service/$catalog_service/setup.sh" 142 + if [[ -f "$setup_script" ]]; then 143 + log_with_prefix "Service Deploy" "Running service setup script..." 144 + # Ensure script is executable 145 + chmod +x "$setup_script" 146 + # Run as service user with .env sourced 147 + if sudo -u "$service_user" -i bash -c "source /mnt/$service_env/.env && bash $setup_script"; then 148 + log_with_prefix "Service Deploy" "Service setup completed" 149 + else 150 + warn_with_prefix "Service Deploy" "Service setup script failed" 151 + fi 152 + fi 153 + 154 + # Start services 155 + local compose_file="/mnt/$service_env/service/$catalog_service/docker-compose.yml" 156 + if [[ -f "$compose_file" ]]; then 157 + log_with_prefix "Service Deploy" "Starting services..." 158 + 159 + # Change to service directory and start with environment loaded via login shell 160 + if sudo -u "$service_user" -i bash -c "source /mnt/$service_env/.env && cd /mnt/$service_env/service/$catalog_service && docker compose --env-file /mnt/$service_env/.env up -d"; then 161 + log_with_prefix "Service Deploy" "✅ Service '$catalog_service' deployed successfully to '$service_env'" 162 + else 163 + error_with_prefix "Service Deploy" "Failed to start services" 164 + exit 1 165 + fi 166 + else 167 + warn_with_prefix "Service Deploy" "No docker-compose.yml found - service copied but not started" 168 + fi 169 + 170 + echo 171 + log_with_prefix "Service Deploy" "Deployment complete!" 172 + echo " Service: $catalog_service" 173 + echo " Ports: $start_port-$end_port" 174 + echo "" 175 + echo "Check status with: tin service status $service_env" 176 + echo "View logs with: tin service logs $service_env $catalog_service" 177 + } 178 + 179 + show_help() { 180 + cat << EOF 181 + tin service deploy - Deploy service from catalog 182 + 183 + USAGE: 184 + tin service deploy <service-env> <catalog-service> 185 + tin service <service-env> <catalog-service> # Shorthand 186 + 187 + DESCRIPTION: 188 + Deploy a service from the catalog to a prepared machine environment. 189 + The service is copied from the catalog and configured for the target 190 + environment with proper UID mapping and port allocation. 191 + 192 + ARGUMENTS: 193 + <service-env> Target machine environment (e.g., gazette-prod) 194 + <catalog-service> Service name from catalog to deploy 195 + 196 + EXAMPLES: 197 + tin service deploy gazette-prod lldap # Deploy LLDAP to gazette-prod 198 + tin service lldap-test redis # Deploy Redis to lldap-test 199 + tin service station-prod prometheus # Deploy Prometheus to station 200 + 201 + NOTES: 202 + - The target machine environment must exist (created with tin machine) 203 + - The catalog service must be available in the service catalog 204 + - Services are deployed as the service user with proper UID/port mapping 205 + 206 + EOF 207 + } 208 + 209 + # Handle help flags 210 + case "${1:-}" in 211 + --help|-h|help) 212 + show_help 213 + exit 0 214 + ;; 215 + esac 216 + 217 + # Main execution 218 + if [[ $# -lt 2 ]]; then 219 + error_with_prefix "Service Deploy" "Service environment and catalog service required" 220 + echo "Usage: tin service deploy <service-env> <catalog-service>" >&2 221 + exit 1 222 + fi 223 + 224 + service_env="$1" 225 + catalog_service="$2" 226 + 227 + deploy_service "$service_env" "$catalog_service"
+99
cmd/service/list.sh
··· 1 + #!/bin/bash 2 + # tin service list - List deployed services 3 + 4 + set -euo pipefail 5 + 6 + # Get tinsnip root and source libraries 7 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 + TINSNIP_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" 9 + source "$TINSNIP_ROOT/lib/core.sh" 10 + 11 + # List deployed services on a specific machine or all machines 12 + list_services() { 13 + local service_env="${1:-}" 14 + 15 + if [[ -n "$service_env" ]]; then 16 + # List services on specific machine 17 + echo "Services on $service_env" 18 + echo "=======================" 19 + echo 20 + 21 + local mount_point="/mnt/$service_env" 22 + if [[ ! -d "$mount_point/service" ]]; then 23 + echo "No services directory found for $service_env" 24 + echo "Machine environment may not exist or have services deployed" 25 + return 1 26 + fi 27 + 28 + local found_services=false 29 + for service_dir in "$mount_point/service"/*; do 30 + if [[ -d "$service_dir" ]]; then 31 + local service_name=$(basename "$service_dir") 32 + echo " $service_name" 33 + 34 + # Check if running via docker compose 35 + if [[ -f "$service_dir/docker-compose.yml" ]]; then 36 + local status="Stopped" 37 + if sudo -u "$service_env" bash -c "cd '$service_dir' && docker compose ps --services --filter status=running" 2>/dev/null | grep -q .; then 38 + status="Running" 39 + fi 40 + echo " Status: $status" 41 + fi 42 + 43 + echo 44 + found_services=true 45 + fi 46 + done 47 + 48 + if [[ "$found_services" == "false" ]]; then 49 + echo "No services deployed on $service_env" 50 + echo 51 + echo "Deploy a service with: tin service deploy $service_env <service-name>" 52 + fi 53 + else 54 + # List all services across all machines 55 + echo "All Service Deployments" 56 + echo "======================" 57 + echo 58 + 59 + local found_any=false 60 + for mount_point in /mnt/*; do 61 + if [[ -d "$mount_point/service" ]]; then 62 + local machine_env=$(basename "$mount_point") 63 + 64 + # Skip if not a valid machine environment format 65 + if [[ ! "$machine_env" =~ ^[^-]+-[^-]+$ ]]; then 66 + continue 67 + fi 68 + 69 + local has_services=false 70 + for service_dir in "$mount_point/service"/*; do 71 + if [[ -d "$service_dir" ]]; then 72 + if [[ "$has_services" == "false" ]]; then 73 + echo "$machine_env:" 74 + has_services=true 75 + found_any=true 76 + fi 77 + 78 + local service_name=$(basename "$service_dir") 79 + echo " $service_name" 80 + fi 81 + done 82 + 83 + if [[ "$has_services" == "true" ]]; then 84 + echo 85 + fi 86 + fi 87 + done 88 + 89 + if [[ "$found_any" == "false" ]]; then 90 + echo "No services deployed on any machine" 91 + echo 92 + echo "Deploy services with: tin service deploy <machine-env> <service-name>" 93 + fi 94 + fi 95 + } 96 + 97 + # Main execution 98 + service_env="${1:-}" 99 + list_services "$service_env"
+71
cmd/service/logs.sh
··· 1 + #!/bin/bash 2 + # tin service logs - Show service logs 3 + 4 + set -euo pipefail 5 + 6 + # Get tinsnip root and source libraries 7 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 + TINSNIP_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" 9 + source "$TINSNIP_ROOT/lib/core.sh" 10 + 11 + # Show service logs 12 + show_service_logs() { 13 + local service_env="$1" 14 + local service_name="${2:-}" 15 + 16 + local mount_point="/mnt/$service_env" 17 + if [[ ! -d "$mount_point/service" ]]; then 18 + error_with_prefix "Service Logs" "No services found on $service_env" 19 + return 1 20 + fi 21 + 22 + if [[ -n "$service_name" ]]; then 23 + # Show logs for specific service 24 + local service_dir="$mount_point/service/$service_name" 25 + if [[ ! -d "$service_dir" ]]; then 26 + error_with_prefix "Service Logs" "Service '$service_name' not found on $service_env" 27 + return 1 28 + fi 29 + 30 + if [[ -f "$service_dir/docker-compose.yml" ]]; then 31 + log_with_prefix "Service Logs" "Showing logs for $service_name on $service_env" 32 + exec sudo -u "$service_env" -i bash -c "source /mnt/$service_env/.env && cd '$service_dir' && docker compose --env-file /mnt/$service_env/.env logs -f" 33 + else 34 + error_with_prefix "Service Logs" "No docker-compose.yml found for $service_name" 35 + return 1 36 + fi 37 + else 38 + # Show logs for all services on the machine 39 + log_with_prefix "Service Logs" "Showing logs for all services on $service_env" 40 + 41 + local found_services=false 42 + for service_dir in "$mount_point/service"/*; do 43 + if [[ -d "$service_dir" && -f "$service_dir/docker-compose.yml" ]]; then 44 + local service=$(basename "$service_dir") 45 + echo "=== $service ===" 46 + sudo -u "$service_env" -i bash -c "source /mnt/$service_env/.env && cd '$service_dir' && docker compose --env-file /mnt/$service_env/.env logs --tail=20" 47 + echo 48 + found_services=true 49 + fi 50 + done 51 + 52 + if [[ "$found_services" == "false" ]]; then 53 + echo "No services with docker-compose found on $service_env" 54 + fi 55 + fi 56 + } 57 + 58 + # Main execution 59 + if [[ $# -eq 0 ]]; then 60 + error_with_prefix "Service Logs" "Service environment required" 61 + echo "Usage: tin service logs <service-env> [service-name]" >&2 62 + echo "Examples:" >&2 63 + echo " tin service logs gazette-prod # All services" >&2 64 + echo " tin service logs gazette-prod lldap # Specific service" >&2 65 + exit 1 66 + fi 67 + 68 + service_env="$1" 69 + service_name="${2:-}" 70 + 71 + show_service_logs "$service_env" "$service_name"
+148
cmd/service/rm.sh
··· 1 + #!/bin/bash 2 + # tin service rm - Remove service deployment 3 + 4 + set -euo pipefail 5 + 6 + # Get tinsnip root and source libraries 7 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 + TINSNIP_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" 9 + source "$TINSNIP_ROOT/lib/core.sh" 10 + source "$TINSNIP_ROOT/lib/metadata.sh" 11 + 12 + # Remove service deployment 13 + remove_service() { 14 + local service_env="$1" 15 + local service_name="$2" 16 + 17 + local mount_point="/mnt/$service_env" 18 + local service_dir="$mount_point/service/$service_name" 19 + 20 + log_with_prefix "Service Remove" "Removing service: $service_name from $service_env" 21 + echo 22 + 23 + # Check if machine exists 24 + if [[ ! -d "$mount_point" ]]; then 25 + error_with_prefix "Service Remove" "Machine environment '$service_env' not found" 26 + exit 1 27 + fi 28 + 29 + # Check if service exists 30 + if [[ ! -d "$service_dir" ]]; then 31 + error_with_prefix "Service Remove" "Service '$service_name' not found on $service_env" 32 + echo "Available services:" >&2 33 + if [[ -d "$mount_point/service" ]]; then 34 + ls -1 "$mount_point/service" 2>/dev/null || echo " (none)" >&2 35 + fi 36 + exit 1 37 + fi 38 + 39 + echo "Service Details:" 40 + echo " Machine: $service_env" 41 + echo " Service: $service_name" 42 + echo " Directory: $service_dir" 43 + echo 44 + 45 + # Check for running containers 46 + local has_compose=false 47 + if [[ -f "$service_dir/docker-compose.yml" ]]; then 48 + has_compose=true 49 + echo "Will stop and remove containers" 50 + fi 51 + echo 52 + 53 + # Confirm removal 54 + read -p "Remove this service deployment? [y/N]: " confirm 55 + case "${confirm}" in 56 + [Yy]) 57 + log_with_prefix "Service Remove" "Proceeding with removal..." 58 + ;; 59 + *) 60 + log_with_prefix "Service Remove" "Removal cancelled" 61 + exit 0 62 + ;; 63 + esac 64 + 65 + # Stop containers if compose file exists 66 + if [[ "$has_compose" == "true" ]]; then 67 + log_with_prefix "Service Remove" "Stopping containers..." 68 + if sudo -u "$service_env" -i bash -c "cd '$service_dir' && docker compose --env-file $mount_point/.env down" 2>/dev/null; then 69 + echo " SUCCESS: Containers stopped and removed" 70 + else 71 + warn_with_prefix "Service Remove" "Failed to stop containers (Docker daemon may not be running)" 72 + echo " Containers will be cleaned up when Docker restarts" 73 + fi 74 + fi 75 + 76 + # Deallocate ports 77 + if deallocate_service_ports "$service_env" "$service_name"; then 78 + log_with_prefix "Service Remove" "Ports deallocated" 79 + else 80 + warn_with_prefix "Service Remove" "Port deallocation failed (non-fatal)" 81 + fi 82 + 83 + # Remove service directory 84 + log_with_prefix "Service Remove" "Removing service directory..." 85 + sudo rm -rf "$service_dir" 86 + 87 + echo 88 + log_with_prefix "Service Remove" "SUCCESS: Service '$service_name' removed from '$service_env'" 89 + echo 90 + echo "Note:" 91 + echo " - Service data on NFS is preserved" 92 + echo " - Ports have been freed for reuse" 93 + echo "" 94 + echo "Service data preserved at:" 95 + echo " Data: $mount_point/data/*/$service_name" 96 + echo " Config: $mount_point/config/*/$service_name" 97 + echo " State: $mount_point/state/*/$service_name" 98 + } 99 + 100 + show_help() { 101 + cat << EOF 102 + tin service rm - Remove service deployment 103 + 104 + USAGE: 105 + tin service rm <service-env> <service-name> 106 + 107 + DESCRIPTION: 108 + Remove a deployed service from a machine environment. 109 + Stops containers and removes service directory. 110 + Service data on NFS is preserved. 111 + 112 + ARGUMENTS: 113 + <service-env> Machine environment (e.g., pds-dev) 114 + <service-name> Service to remove (e.g., pds) 115 + 116 + EXAMPLES: 117 + tin service rm pds-dev pds # Remove PDS from pds-dev 118 + tin service rm gateway-prod caddy # Remove Caddy from gateway-prod 119 + 120 + NOTES: 121 + - Containers are stopped and removed 122 + - Service deployment directory is deleted 123 + - Service data (config/data/state) is PRESERVED on NAS 124 + - Can redeploy service to restore from preserved data 125 + 126 + EOF 127 + } 128 + 129 + # Handle help flags 130 + case "${1:-}" in 131 + --help|-h|help) 132 + show_help 133 + exit 0 134 + ;; 135 + esac 136 + 137 + # Main execution 138 + if [[ $# -lt 2 ]]; then 139 + error_with_prefix "Service Remove" "Service environment and service name required" 140 + echo "Usage: tin service rm <service-env> <service-name>" >&2 141 + echo "Example: tin service rm pds-dev pds" >&2 142 + exit 1 143 + fi 144 + 145 + service_env="$1" 146 + service_name="$2" 147 + 148 + remove_service "$service_env" "$service_name"
+74
cmd/service/status.sh
··· 1 + #!/bin/bash 2 + # tin service status - Show service deployment status 3 + 4 + set -euo pipefail 5 + 6 + # Get tinsnip root and source libraries 7 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 + TINSNIP_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" 9 + source "$TINSNIP_ROOT/lib/core.sh" 10 + 11 + # Show service deployment status 12 + show_service_status() { 13 + local service_env="$1" 14 + 15 + echo "Service Status: $service_env" 16 + echo "=============================" 17 + echo 18 + 19 + local mount_point="/mnt/$service_env" 20 + if [[ ! -d "$mount_point" ]]; then 21 + error_with_prefix "Service Status" "Machine environment '$service_env' not found" 22 + echo "Create with: tin machine create <service> <environment>" >&2 23 + return 1 24 + fi 25 + 26 + if [[ ! -d "$mount_point/service" ]]; then 27 + echo "No services deployed on $service_env" 28 + echo 29 + echo "Deploy services with: tin service deploy $service_env <service-name>" 30 + return 0 31 + fi 32 + 33 + local found_services=false 34 + for service_dir in "$mount_point/service"/*; do 35 + if [[ -d "$service_dir" ]]; then 36 + local service_name=$(basename "$service_dir") 37 + echo "Service: $service_name" 38 + echo " Directory: $service_dir" 39 + 40 + if [[ -f "$service_dir/docker-compose.yml" ]]; then 41 + echo " Compose file: ✅" 42 + 43 + # Check container status 44 + local container_output 45 + if container_output=$(sudo -u "$service_env" -i bash -c "cd '$service_dir' && docker compose --env-file /mnt/$service_env/.env ps --format table" 2>/dev/null); then 46 + echo " Containers:" 47 + echo "$container_output" | tail -n +2 | sed 's/^/ /' 48 + else 49 + echo " Containers: ❌ (failed to query)" 50 + fi 51 + else 52 + echo " Compose file: ❌" 53 + fi 54 + 55 + echo 56 + found_services=true 57 + fi 58 + done 59 + 60 + if [[ "$found_services" == "false" ]]; then 61 + echo "No services found in $mount_point/service" 62 + fi 63 + } 64 + 65 + # Main execution 66 + if [[ $# -eq 0 ]]; then 67 + error_with_prefix "Service Status" "Service environment required" 68 + echo "Usage: tin service status <service-env>" >&2 69 + echo "Example: tin service status gazette-prod" >&2 70 + exit 1 71 + fi 72 + 73 + service_env="$1" 74 + show_service_status "$service_env"
+113
cmd/service/stop.sh
··· 1 + #!/bin/bash 2 + # tin service stop - Stop service containers 3 + 4 + set -euo pipefail 5 + 6 + # Get tinsnip root and source libraries 7 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8 + TINSNIP_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" 9 + source "$TINSNIP_ROOT/lib/core.sh" 10 + 11 + # Stop service containers 12 + stop_service() { 13 + local service_env="$1" 14 + local service_name="${2:-}" 15 + 16 + local mount_point="/mnt/$service_env" 17 + if [[ ! -d "$mount_point/service" ]]; then 18 + error_with_prefix "Service Stop" "No services found on $service_env" 19 + return 1 20 + fi 21 + 22 + if [[ -n "$service_name" ]]; then 23 + # Stop specific service 24 + local service_dir="$mount_point/service/$service_name" 25 + if [[ ! -d "$service_dir" ]]; then 26 + error_with_prefix "Service Stop" "Service '$service_name' not found on $service_env" 27 + return 1 28 + fi 29 + 30 + if [[ -f "$service_dir/docker-compose.yml" ]]; then 31 + log_with_prefix "Service Stop" "Stopping $service_name on $service_env..." 32 + if sudo -u "$service_env" -i bash -c "cd '$service_dir' && docker compose --env-file /mnt/$service_env/.env down"; then 33 + log_with_prefix "Service Stop" "✅ Stopped $service_name" 34 + else 35 + error_with_prefix "Service Stop" "Failed to stop $service_name" 36 + return 1 37 + fi 38 + else 39 + error_with_prefix "Service Stop" "No docker-compose.yml found for $service_name" 40 + return 1 41 + fi 42 + else 43 + # Stop all services on the machine 44 + log_with_prefix "Service Stop" "Stopping all services on $service_env..." 45 + 46 + local found_services=false 47 + for service_dir in "$mount_point/service"/*; do 48 + if [[ -d "$service_dir" && -f "$service_dir/docker-compose.yml" ]]; then 49 + local service=$(basename "$service_dir") 50 + echo "Stopping $service..." 51 + if sudo -u "$service_env" -i bash -c "cd '$service_dir' && docker compose --env-file /mnt/$service_env/.env down"; then 52 + echo " ✅ $service stopped" 53 + else 54 + echo " ❌ $service failed to stop" 55 + fi 56 + found_services=true 57 + fi 58 + done 59 + 60 + if [[ "$found_services" == "false" ]]; then 61 + echo "No services with docker-compose found on $service_env" 62 + fi 63 + fi 64 + } 65 + 66 + show_help() { 67 + cat << EOF 68 + tin service stop - Stop service containers 69 + 70 + USAGE: 71 + tin service stop <service-env> [service-name] 72 + 73 + DESCRIPTION: 74 + Stop running service containers. Removes containers but preserves data. 75 + 76 + ARGUMENTS: 77 + <service-env> Target machine environment (e.g., gazette-prod) 78 + [service-name] Specific service to stop (optional, stops all if omitted) 79 + 80 + EXAMPLES: 81 + tin service stop pds-dev pds # Stop specific service 82 + tin service stop pds-dev # Stop all services on pds-dev 83 + 84 + NOTES: 85 + - Containers are removed but volumes/data are preserved 86 + - To restart: tin service deploy <service-env> <service-name> 87 + - Use 'tin service status' to check container states 88 + 89 + EOF 90 + } 91 + 92 + # Handle help flags 93 + case "${1:-}" in 94 + --help|-h|help) 95 + show_help 96 + exit 0 97 + ;; 98 + esac 99 + 100 + # Main execution 101 + if [[ $# -eq 0 ]]; then 102 + error_with_prefix "Service Stop" "Service environment required" 103 + echo "Usage: tin service stop <service-env> [service-name]" >&2 104 + echo "Examples:" >&2 105 + echo " tin service stop pds-dev pds # Stop specific service" >&2 106 + echo " tin service stop pds-dev # Stop all services" >&2 107 + exit 1 108 + fi 109 + 110 + service_env="$1" 111 + service_name="${2:-}" 112 + 113 + stop_service "$service_env" "$service_name"
-52
lib/core.sh
··· 232 232 return 1 233 233 } 234 234 235 - # Allocate ports for a service and append to .env file 236 - # Usage: allocate_service_ports <num_ports> 237 - # This function should be called from a service's setup.sh 238 - # It reads TIN_SERVICE_UID from the environment and allocates ports 239 - allocate_service_ports() { 240 - local num_ports="${1:-1}" 241 - local env_file="${2:-/mnt/$(whoami)/.env}" 242 - 243 - # Validate we're running as a service user 244 - if [[ ! "$(whoami)" =~ ^[^-]+-[^-]+$ ]]; then 245 - echo "ERROR: allocate_service_ports must be called from service user context" >&2 246 - return 1 247 - fi 248 - 249 - # Ensure TIN_SERVICE_UID is set 250 - if [[ -z "${TIN_SERVICE_UID:-}" ]]; then 251 - echo "ERROR: TIN_SERVICE_UID not set. Source .env first." >&2 252 - return 1 253 - fi 254 - 255 - # Validate port count 256 - if [[ $num_ports -lt 1 || $num_ports -gt 10 ]]; then 257 - echo "ERROR: num_ports must be 1-10 (SMEP allows max 10 ports per machine)" >&2 258 - return 1 259 - fi 260 - 261 - echo "Allocating $num_ports port(s) for service (base UID: $TIN_SERVICE_UID)" 262 - 263 - # Check if ports are already defined in .env (avoid duplicates) 264 - if grep -q "^TIN_PORT_0=" "$env_file" 2>/dev/null; then 265 - echo "Ports already allocated in $env_file, skipping" 266 - return 0 267 - fi 268 - 269 - # Append port allocations to .env 270 - { 271 - echo "" 272 - echo "# Port allocations (added by service setup)" 273 - for ((i=0; i<num_ports; i++)); do 274 - echo "TIN_PORT_${i}=$((TIN_SERVICE_UID + i))" 275 - done 276 - } >> "$env_file" 277 - 278 - echo "✓ Allocated ports $TIN_SERVICE_UID through $((TIN_SERVICE_UID + num_ports - 1))" 279 - 280 - # Re-export the port variables for immediate use 281 - for ((i=0; i<num_ports; i++)); do 282 - export "TIN_PORT_${i}=$((TIN_SERVICE_UID + i))" 283 - done 284 - 285 - return 0 286 - }
+277
lib/metadata.sh
··· 183 183 return 0 # Non-fatal - Docker setup may not always be needed 184 184 fi 185 185 } 186 + 187 + # ============================================================================ 188 + # Port Allocation Functions 189 + # ============================================================================ 190 + 191 + # Get number of ports a service needs by parsing docker-compose.yml 192 + # Returns count of unique TIN_PORT_* references, defaults to 1 if none found 193 + get_service_port_count() { 194 + local compose_file="$1" 195 + 196 + if [[ ! -f "$compose_file" ]]; then 197 + log_with_prefix "Port Count" "docker-compose.yml not found, defaulting to 1 port" 198 + echo "1" 199 + return 0 200 + fi 201 + 202 + # Extract unique TIN_PORT_* variable references 203 + local port_count=$(grep -o '\${TIN_PORT_[0-9]\+}' "$compose_file" 2>/dev/null | \ 204 + sed 's/\${TIN_PORT_\([0-9]\+\)}/\1/' | \ 205 + sort -u | \ 206 + wc -l | \ 207 + tr -d ' ') 208 + 209 + # Default to 1 if no ports found 210 + if [[ -z "$port_count" || "$port_count" -eq 0 ]]; then 211 + log_with_prefix "Port Count" "No TIN_PORT_* references found, defaulting to 1 port" 212 + echo "1" 213 + else 214 + log_with_prefix "Port Count" "Service requires $port_count port(s)" 215 + echo "$port_count" 216 + fi 217 + 218 + return 0 219 + } 220 + 221 + # Read port allocations from registry file 222 + # Output format: One line per allocation: "service_name start_port num_ports" 223 + read_port_allocations() { 224 + local ports_file="$1" 225 + 226 + if [[ ! -f "$ports_file" ]]; then 227 + # Empty registry 228 + return 0 229 + fi 230 + 231 + # Parse registry, skip comments and empty lines 232 + grep -v '^#' "$ports_file" | grep -v '^[[:space:]]*$' || true 233 + } 234 + 235 + # Find next available port range using first-fit algorithm 236 + # Returns start_port or error if exhausted 237 + find_available_port_range() { 238 + local ports_file="$1" 239 + local machine_uid="$2" 240 + local num_ports="$3" 241 + 242 + local max_port=$((machine_uid + 9)) 243 + local min_port=$machine_uid 244 + 245 + # Read existing allocations and sort by start_port 246 + local allocations=$(read_port_allocations "$ports_file" | sort -t: -k2 -n) 247 + 248 + # Track current position (start of search range) 249 + local current=$min_port 250 + 251 + # Check each allocation to find gaps 252 + while IFS=: read -r service start num; do 253 + [[ -z "$service" ]] && continue 254 + 255 + local start_port=$((start)) 256 + local allocated_num=$((num)) 257 + 258 + # Is there a gap before this allocation? 259 + if [[ $((start_port - current)) -ge $num_ports ]]; then 260 + # Found a gap that fits! 261 + echo "$current" 262 + return 0 263 + fi 264 + 265 + # Move past this allocation 266 + current=$((start_port + allocated_num)) 267 + done <<< "$allocations" 268 + 269 + # Check if there's space after the last allocation 270 + if [[ $((max_port - current + 1)) -ge $num_ports ]]; then 271 + echo "$current" 272 + return 0 273 + fi 274 + 275 + # No space available 276 + return 1 277 + } 278 + 279 + # Allocate ports for a catalog service 280 + # Returns allocated start_port or exits on error 281 + allocate_service_ports() { 282 + local machine_name="$1" 283 + local machine_env="$2" 284 + local catalog_service="$3" 285 + local num_ports="$4" 286 + 287 + local sheet="${TIN_SHEET:-topsheet}" 288 + local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$machine_env") 289 + local ports_file="$metadata_dir/ports" 290 + 291 + # Get machine UID 292 + local machine_uid 293 + if ! machine_uid=$(calculate_service_uid "$machine_name" "$machine_env" 2>/dev/null); then 294 + error_with_prefix "Port Allocation" "Cannot calculate UID for machine" 295 + return 1 296 + fi 297 + 298 + local max_port=$((machine_uid + 9)) 299 + 300 + log_with_prefix "Port Allocation" "Allocating $num_ports port(s) for $catalog_service" 301 + 302 + # Validate port count 303 + if [[ $num_ports -lt 1 || $num_ports -gt 10 ]]; then 304 + error_with_prefix "Port Allocation" "Invalid port count: $num_ports (must be 1-10)" 305 + return 1 306 + fi 307 + 308 + # Check if service already allocated 309 + if read_port_allocations "$ports_file" | grep -q "^${catalog_service}:"; then 310 + error_with_prefix "Port Allocation" "Service '$catalog_service' already deployed to ${machine_name}-${machine_env}" 311 + echo " Current allocation:" >&2 312 + grep "^${catalog_service}:" "$ports_file" | while IFS=: read -r svc start num; do 313 + echo " Ports: $start-$((start + num - 1)) ($num ports)" >&2 314 + done 315 + echo "" >&2 316 + echo " To redeploy:" >&2 317 + echo " 1. Remove: tin service rm ${machine_name}-${machine_env} $catalog_service" >&2 318 + echo " 2. Deploy: tin service deploy ${machine_name}-${machine_env} $catalog_service" >&2 319 + return 1 320 + fi 321 + 322 + # Find available port range 323 + local start_port 324 + if ! start_port=$(find_available_port_range "$ports_file" "$machine_uid" "$num_ports"); then 325 + error_with_prefix "Port Allocation" "Insufficient ports available" 326 + echo " Machine: ${machine_name}-${machine_env} (UID $machine_uid)" >&2 327 + echo " Port range: $machine_uid-$max_port (10 ports total)" >&2 328 + echo " Requested: $num_ports ports" >&2 329 + echo "" >&2 330 + echo " Current allocations:" >&2 331 + read_port_allocations "$ports_file" | while IFS=: read -r svc start num; do 332 + echo " $svc: $start-$((start + num - 1)) ($num ports)" >&2 333 + done 334 + echo "" >&2 335 + echo " Suggestions:" >&2 336 + echo " - Remove unused services: tin service rm ${machine_name}-${machine_env} <service>" >&2 337 + echo " - Create new machine: tin machine create ${machine_name}2 $machine_env <nas-server>" >&2 338 + return 1 339 + fi 340 + 341 + # Validate allocation doesn't exceed max port 342 + local end_port=$((start_port + num_ports - 1)) 343 + if [[ $end_port -gt $max_port ]]; then 344 + error_with_prefix "Port Allocation" "Port allocation would exceed machine range" 345 + echo " Calculated range: $start_port-$end_port" >&2 346 + echo " Maximum allowed: $max_port" >&2 347 + return 1 348 + fi 349 + 350 + # Append to registry (will be sorted) 351 + local temp_file=$(mktemp) 352 + 353 + # Copy header and existing allocations 354 + if [[ -f "$ports_file" ]]; then 355 + grep '^#' "$ports_file" > "$temp_file" || true 356 + read_port_allocations "$ports_file" >> "$temp_file" 357 + fi 358 + 359 + # Add new allocation 360 + echo "${catalog_service}:${start_port}:${num_ports}" >> "$temp_file" 361 + 362 + # Sort allocations by start_port and write back 363 + ( 364 + grep '^#' "$temp_file" || true 365 + grep -v '^#' "$temp_file" | grep -v '^[[:space:]]*$' | sort -t: -k2 -n || true 366 + ) | sudo -u station-prod tee "$ports_file" > /dev/null 367 + 368 + rm -f "$temp_file" 369 + 370 + log_with_prefix "Port Allocation" "✓ Allocated ports $start_port-$end_port ($num_ports ports)" 371 + 372 + echo "$start_port" 373 + return 0 374 + } 375 + 376 + # Deallocate ports for a catalog service 377 + deallocate_service_ports() { 378 + local machine_env="$1" 379 + local catalog_service="$2" 380 + 381 + # Parse machine-environment 382 + local parsed_output 383 + if ! parsed_output=$(parse_machine_name "$machine_env" 2>/dev/null); then 384 + error_with_prefix "Port Deallocation" "Invalid machine environment: $machine_env" 385 + return 1 386 + fi 387 + 388 + local machine_name=$(echo "$parsed_output" | sed -n '1p') 389 + local environment=$(echo "$parsed_output" | sed -n '2p') 390 + 391 + local metadata_dir=$(get_machine_metadata_dir "$machine_name" "$environment") 392 + local ports_file="$metadata_dir/ports" 393 + 394 + if [[ ! -f "$ports_file" ]]; then 395 + log_with_prefix "Port Deallocation" "No ports registry found, skipping" 396 + return 0 397 + fi 398 + 399 + # Check if service has allocation 400 + if ! grep -q "^${catalog_service}:" "$ports_file"; then 401 + log_with_prefix "Port Deallocation" "No port allocation found for $catalog_service" 402 + return 0 403 + fi 404 + 405 + log_with_prefix "Port Deallocation" "Freeing ports for $catalog_service" 406 + 407 + # Get allocation details for logging 408 + local allocation=$(grep "^${catalog_service}:" "$ports_file") 409 + local start_port=$(echo "$allocation" | cut -d: -f2) 410 + local num_ports=$(echo "$allocation" | cut -d: -f3) 411 + local end_port=$((start_port + num_ports - 1)) 412 + 413 + # Remove allocation (preserve header, remove service line) 414 + local temp_file=$(mktemp) 415 + grep -v "^${catalog_service}:" "$ports_file" | sudo -u station-prod tee "$temp_file" > /dev/null 416 + sudo -u station-prod mv "$temp_file" "$ports_file" 417 + 418 + log_with_prefix "Port Deallocation" "✓ Freed ports $start_port-$end_port ($num_ports ports)" 419 + 420 + return 0 421 + } 422 + 423 + # Generate service .env file with port allocations 424 + generate_service_env() { 425 + local service_env="$1" 426 + local catalog_service="$2" 427 + local start_port="$3" 428 + local num_ports="$4" 429 + 430 + local service_dir="/mnt/${service_env}/service/${catalog_service}" 431 + local env_file="${service_dir}/.env" 432 + 433 + log_with_prefix "Service Env" "Generating service .env file" 434 + 435 + # Ensure service directory exists 436 + if [[ ! -d "$service_dir" ]]; then 437 + error_with_prefix "Service Env" "Service directory not found: $service_dir" 438 + return 1 439 + fi 440 + 441 + local end_port=$((start_port + num_ports - 1)) 442 + 443 + # Generate .env file as service user 444 + sudo -u "$service_env" tee "$env_file" > /dev/null << EOF 445 + # Tinsnip Service Environment 446 + # Catalog Service: ${catalog_service} 447 + # Machine: ${service_env} 448 + # Allocated Ports: ${start_port}-${end_port} (${num_ports} ports) 449 + # Generated: $(date '+%Y-%m-%d %H:%M:%S') 450 + 451 + TIN_CATALOG_SERVICE=${catalog_service} 452 + EOF 453 + 454 + # Append port variables 455 + for ((i=0; i<num_ports; i++)); do 456 + echo "TIN_PORT_${i}=$((start_port + i))" | sudo -u "$service_env" tee -a "$env_file" > /dev/null 457 + done 458 + 459 + log_with_prefix "Service Env" "✓ Created: $env_file" 460 + 461 + return 0 462 + }
+28 -5
ooda/2025-10-multi-service-architecture/act/03-port-allocation/BRANCH.md
··· 1 1 # Branch Status: Port Allocation 2 2 3 3 **Branch**: `feature/port-allocation-algorithm` 4 - **Status**: [...] Not Started 5 - **Dependencies**: ACT-2 must be merged first 4 + **Status**: [~] In Progress 5 + **Started**: 2025-10-22 6 + **Dependencies**: ACT-2 ✅ Complete 6 7 7 8 ## Quick Links 8 9 9 10 - **Plan**: [plan.md](./plan.md) 10 11 - **ORIENT**: [../../orient/port-allocation-algorithm.md](../../orient/port-allocation-algorithm.md) 11 - - **PR**: TBD 12 + - **Merge Commit**: TBD 13 + 14 + ## Timeline 15 + 16 + | Event | Date | Notes | 17 + |-------|------|-------| 18 + | Plan created | 2025-10-22 | plan.md and outcomes.md updated | 19 + | Branch created | 2025-10-22 | feature/port-allocation-algorithm | 20 + | Implementation | 2025-10-22 | Port allocation functions added | 21 + | Testing | 2025-10-22 | Ready for geneticalgorithm testing | 22 + 23 + ## Testing 12 24 13 - ## Status 25 + - [ ] Deploy multiple services to one machine 26 + - [ ] Verify port registry tracking 27 + - [ ] Verify service .env generation 28 + - [ ] Test port deallocation on service removal 29 + - [ ] Test first-fit reallocation 30 + - [ ] Test port exhaustion error 14 31 15 - [...] **Blocked** - Waiting for ACT-2 (machine metadata) to complete 32 + ## Issues Encountered 33 + 34 + (To be filled during testing) 35 + 36 + ## Notes 37 + 38 + Implementation complete, ready for testing on geneticalgorithm.
+358
ooda/2025-10-multi-service-architecture/act/03-port-allocation/plan.md
··· 1 + # ACT-3: Port Allocation Algorithm Implementation Plan 2 + 3 + **Branch**: `feature/port-allocation-algorithm` 4 + **ORIENT Design**: [../../orient/port-allocation-algorithm.md](../../orient/port-allocation-algorithm.md) 5 + **Dependencies**: ACT-2 (machine metadata) ✅ Complete 6 + 7 + ## Objective 8 + 9 + Implement coordinated port allocation so multiple catalog services can deploy to one machine without port conflicts. 10 + 11 + ## Current State 12 + 13 + **Machine creation (`tin machine create homelab prod`):** 14 + - ✅ Creates machine metadata in station-prod (ACT-2) 15 + - ✅ Empty `ports` registry file exists 16 + - ❌ No port allocation logic 17 + - ❌ No service .env generation 18 + 19 + **Service deployment (`tin service deploy homelab-prod lldap`):** 20 + - ✅ Copies service files 21 + - ❌ Does not allocate ports from registry 22 + - ❌ Does not generate service .env 23 + - ⚠️ Old `allocate_service_ports()` in lib/core.sh (broken for multi-service) 24 + 25 + **Problems:** 26 + - Services cannot coordinate port usage 27 + - No central tracking of allocations 28 + - Port conflicts when deploying multiple services 29 + 30 + ## Target State 31 + 32 + **Directory Structure After Deployment:** 33 + ``` 34 + /mnt/station-prod/data/machines/topsheet/homelab-prod/ 35 + ├── machine.env # Machine infrastructure vars (unchanged) 36 + └── ports # Port allocation registry (POPULATED) 37 + # lldap:50300:2 38 + # caddy:50302:2 39 + # redis:50304:1 40 + 41 + /mnt/homelab-prod/service/ 42 + ├── lldap/ 43 + │ ├── .env # NEW: Service-specific ports 44 + │ │ # TIN_CATALOG_SERVICE=lldap 45 + │ │ # TIN_PORT_0=50300 46 + │ │ # TIN_PORT_1=50301 47 + │ └── docker-compose.yml 48 + ├── caddy/ 49 + │ ├── .env # NEW: Service-specific ports 50 + │ │ # TIN_PORT_0=50302 51 + │ │ # TIN_PORT_1=50303 52 + │ └── docker-compose.yml 53 + └── redis/ 54 + ├── .env # NEW: Service-specific ports 55 + │ # TIN_PORT_0=50304 56 + └── docker-compose.yml 57 + ``` 58 + 59 + **Benefits:** 60 + - Multiple services share machine without conflicts 61 + - Central registry provides observability 62 + - First-fit algorithm supports deallocation/reuse 63 + - Clear error messages on port exhaustion 64 + 65 + ## Implementation Checklist 66 + 67 + ### Phase 1: Port Allocation Functions (lib/metadata.sh) 68 + 69 + - [ ] **Add `get_service_port_count()`** 70 + - Parse docker-compose.yml for TIN_PORT_* references 71 + - Return count of unique port variables 72 + - Default to 1 if no references found 73 + 74 + - [ ] **Add `read_port_allocations()`** 75 + - Parse ports registry file 76 + - Return associative array: service -> (start_port, num_ports) 77 + - Skip comments and empty lines 78 + 79 + - [ ] **Add `find_available_port_range()`** 80 + - Implement first-fit gap-finding algorithm 81 + - Input: allocations, machine_uid, num_ports_needed 82 + - Output: start_port or error if exhausted 83 + 84 + - [ ] **Add `allocate_service_ports()`** (NEW implementation) 85 + - Read current allocations 86 + - Find available range 87 + - Validate within machine's 10-port limit 88 + - Append to ports registry (sorted by start_port) 89 + - Return allocated start_port 90 + 91 + - [ ] **Add `deallocate_service_ports()`** 92 + - Remove service entry from ports registry 93 + - Preserve header comments 94 + - Maintain sorted order 95 + 96 + - [ ] **Add `generate_service_env()`** 97 + - Create service/.env with TIN_PORT_* variables 98 + - Include TIN_CATALOG_SERVICE 99 + - Include allocation comment header 100 + 101 + ### Phase 2: Update Service Deployment (cmd/service/deploy.sh) 102 + 103 + - [ ] **Source lib/metadata.sh** 104 + - Add after existing library sources 105 + 106 + - [ ] **After copying service files (line ~113)** 107 + - Determine port count: `port_count=$(get_service_port_count "$compose_file")` 108 + - Allocate ports: `start_port=$(allocate_service_ports "$machine_name" "$environment" "$catalog_service" "$port_count")` 109 + - Generate service .env: `generate_service_env "$service_env" "$catalog_service" "$start_port" "$port_count"` 110 + - Handle errors (port exhaustion, invalid compose file) 111 + 112 + - [ ] **Keep existing .env sourcing** (lines 122, 135) 113 + - Leave for backward compatibility 114 + - Note: Will be replaced by env_file directive in ACT-4 115 + 116 + - [ ] **Update deployment success message** 117 + - Show allocated port range 118 + 119 + ### Phase 3: Update Service Removal (cmd/service/rm.sh) 120 + 121 + - [ ] **Source lib/metadata.sh** 122 + - Add after existing library sources 123 + 124 + - [ ] **After stopping containers (line ~72)** 125 + - Deallocate ports: `deallocate_service_ports "$service_env" "$service_name"` 126 + - Update success message to mention freed ports 127 + 128 + ### Phase 4: Remove Old Implementation (lib/core.sh) 129 + 130 + - [ ] **Remove old `allocate_service_ports()`** (lines 236-286) 131 + - No stub needed - clean removal 132 + - New implementation in lib/metadata.sh handles this 133 + 134 + ### Phase 5: Testing on geneticalgorithm 135 + 136 + - [ ] **Reinstall tinsnip on test machine** 137 + ```bash 138 + ssh geneticalgorithm 'curl -fsSL "http://192.168.0.218:8000/install.sh?$(date +%s)" | REPO_URL="http://192.168.0.218:8000" bash' 139 + ``` 140 + 141 + - [ ] **Create test machine (INTERACTIVE - user must run)** 142 + ```bash 143 + # User runs on geneticalgorithm: 144 + TIN_SHEET=dynamicalsystem.com tin machine create test-ports prod ds412plus.local 145 + ``` 146 + 147 + - [ ] **Verify ports registry initialized** 148 + ```bash 149 + ssh geneticalgorithm 'cat /mnt/station-prod/data/machines/dynamicalsystem.com/test-ports-prod/ports' 150 + # Expected: Empty registry with header 151 + ``` 152 + 153 + - [ ] **Deploy first service (INTERACTIVE - user must run)** 154 + ```bash 155 + # User runs on geneticalgorithm: 156 + TIN_SHEET=dynamicalsystem.com tin service deploy test-ports-prod lldap 157 + ``` 158 + 159 + - [ ] **Verify first allocation** 160 + ```bash 161 + ssh geneticalgorithm 'cat /mnt/station-prod/data/machines/dynamicalsystem.com/test-ports-prod/ports' 162 + # Expected: lldap:50X00:2 163 + 164 + ssh geneticalgorithm 'cat /mnt/test-ports-prod/service/lldap/.env' 165 + # Expected: TIN_PORT_0=50X00, TIN_PORT_1=50X01 166 + ``` 167 + 168 + - [ ] **Deploy second service (INTERACTIVE)** 169 + ```bash 170 + # User runs on geneticalgorithm: 171 + TIN_SHEET=dynamicalsystem.com tin service deploy test-ports-prod redis 172 + ``` 173 + 174 + - [ ] **Verify second allocation** 175 + ```bash 176 + ssh geneticalgorithm 'cat /mnt/station-prod/data/machines/dynamicalsystem.com/test-ports-prod/ports' 177 + # Expected: lldap:50X00:2 178 + # redis:50X02:1 179 + ``` 180 + 181 + - [ ] **Remove first service and verify deallocation** 182 + ```bash 183 + ssh geneticalgorithm 'yes | TIN_SHEET=dynamicalsystem.com tin service rm test-ports-prod lldap' 184 + ssh geneticalgorithm 'cat /mnt/station-prod/data/machines/dynamicalsystem.com/test-ports-prod/ports' 185 + # Expected: redis:50X02:1 (lldap removed) 186 + ``` 187 + 188 + - [ ] **Redeploy service into freed space** 189 + ```bash 190 + # User runs on geneticalgorithm: 191 + TIN_SHEET=dynamicalsystem.com tin service deploy test-ports-prod lldap 192 + 193 + ssh geneticalgorithm 'cat /mnt/station-prod/data/machines/dynamicalsystem.com/test-ports-prod/ports' 194 + # Expected: lldap:50X00:2 (first-fit reused freed space) 195 + # redis:50X02:1 196 + ``` 197 + 198 + - [ ] **Test port exhaustion error** 199 + ```bash 200 + # Deploy services until exhausted (10 ports total) 201 + # Verify clear error message with actionable guidance 202 + ``` 203 + 204 + ### Phase 6: Documentation 205 + 206 + - [ ] **Update BRANCH.md** 207 + - Mark status as in_progress when starting 208 + - Add timeline events 209 + - Document issues encountered 210 + - Mark complete when merged 211 + 212 + - [ ] **Update outcomes.md** 213 + - Add test results 214 + - Mark verification checklist 215 + 216 + - [ ] **Update main README if needed** 217 + - Document port allocation behavior 218 + 219 + ## File Changes 220 + 221 + ### New Functions (lib/metadata.sh) 222 + 223 + ```bash 224 + # Around line 186 (after existing functions): 225 + get_service_port_count() 226 + read_port_allocations() 227 + find_available_port_range() 228 + allocate_service_ports() 229 + deallocate_service_ports() 230 + generate_service_env() 231 + ``` 232 + 233 + ### Modified Files 234 + 235 + ``` 236 + lib/metadata.sh # Add port allocation functions 237 + cmd/service/deploy.sh # Call allocation, generate service .env 238 + cmd/service/rm.sh # Deallocate ports 239 + lib/core.sh # Remove old allocate_service_ports() 240 + ``` 241 + 242 + ### Unchanged Files (for now) 243 + 244 + ``` 245 + service/*/docker-compose.yml # ACT-4 will add env_file directive 246 + ``` 247 + 248 + ## Service .env Format 249 + 250 + ```bash 251 + # Tinsnip Service Environment 252 + # Catalog Service: lldap 253 + # Machine: homelab-prod 254 + # Allocated Ports: 50300-50301 (2 ports) 255 + # Generated: 2025-10-22 16:00:00 256 + 257 + TIN_CATALOG_SERVICE=lldap 258 + TIN_PORT_0=50300 259 + TIN_PORT_1=50301 260 + ``` 261 + 262 + **Ownership**: Machine user (e.g., `homelab-prod:homelab-prod`) 263 + **Permissions**: `644` (readable, writable by machine user) 264 + 265 + ## Error Messages 266 + 267 + ### Port Exhaustion 268 + ``` 269 + ERROR: Insufficient ports available 270 + Machine: homelab-prod (UID 50300) 271 + Port range: 50300-50309 (10 ports total) 272 + Requested: 3 ports 273 + Available: 2 ports 274 + Current allocations: 275 + lldap: 50300-50301 (2 ports) 276 + caddy: 50302-50303 (2 ports) 277 + redis: 50304-50309 (6 ports) 278 + 279 + Suggestions: 280 + - Remove unused services: tin service rm homelab-prod <service> 281 + - Create new machine: tin machine create homelab2 prod <nas-server> 282 + ``` 283 + 284 + ### Service Already Deployed 285 + ``` 286 + ERROR: Service 'lldap' already deployed to homelab-prod 287 + Current allocation: ports 50300-50301 (2 ports) 288 + 289 + To redeploy: 290 + 1. Remove: tin service rm homelab-prod lldap 291 + 2. Deploy: tin service deploy homelab-prod lldap 292 + ``` 293 + 294 + ### Invalid docker-compose.yml 295 + ``` 296 + ERROR: Cannot determine port count for service 'lldap' 297 + File: /mnt/homelab-prod/service/lldap/docker-compose.yml 298 + Reason: File not found or not readable 299 + ``` 300 + 301 + ## Algorithm Implementation Notes 302 + 303 + ### First-Fit Port Allocation 304 + 305 + 1. Read existing allocations from ports registry 306 + 2. Sort allocations by start_port 307 + 3. Find gaps: 308 + - Before first allocation 309 + - Between allocations 310 + - After last allocation 311 + 4. Use first gap that fits requested port count 312 + 5. Validate allocation doesn't exceed UID+9 313 + 6. Write to registry maintaining sorted order 314 + 315 + ### Port Count Detection 316 + 317 + Parse docker-compose.yml for TIN_PORT_* variable references: 318 + ```bash 319 + grep -o '\${TIN_PORT_[0-9]\+}' docker-compose.yml | sort -u | wc -l 320 + ``` 321 + 322 + If no references found, default to 1 port. 323 + 324 + ## Key Design Decisions 325 + 326 + 1. **First-fit algorithm**: Simple, supports fragmentation, good enough for 10-port range 327 + 2. **Parse docker-compose.yml**: No separate metadata file needed 328 + 3. **Generate service .env**: tin owns this file, services read it 329 + 4. **Keep old .env sourcing**: Maintain working state for ACT-4 transition 330 + 5. **Clean removal**: No backward compat stubs for old allocate_service_ports() 331 + 332 + ## Testing Strategy 333 + 334 + ### Unit Tests (Future) 335 + - Test port count parsing with various docker-compose.yml formats 336 + - Test gap-finding algorithm with various allocation patterns 337 + - Test first-fit allocation edge cases 338 + - Test port exhaustion handling 339 + 340 + ### Integration Tests (Manual - geneticalgorithm) 341 + - Deploy 3 services to one machine 342 + - Verify port allocations in registry 343 + - Remove middle service, verify deallocation 344 + - Redeploy service, verify first-fit reuse 345 + - Attempt to exhaust ports, verify error 346 + 347 + ### Regression Tests 348 + - Verify existing single-service deployments still work 349 + - Verify machine creation unchanged 350 + - Verify service removal unchanged (except port deallocation added) 351 + 352 + ## Notes 353 + 354 + - This ACT does NOT change docker-compose.yml files 355 + - Services will still use old --env-file pattern until ACT-4 356 + - Focus: Port coordination, not docker-compose integration yet 357 + - Keep changes minimal and testable 358 + - Build foundation for ACT-4 (env_file directive)
+60 -1
ooda/2025-10-multi-service-architecture/outcomes.md
··· 71 71 72 72 ## port-allocation 73 73 74 - placeholder 74 + ### Outcome 75 + 76 + Enable multiple catalog services to coexist on one machine with coordinated port allocation. 77 + 78 + ### Test 79 + 80 + An operator can deploy multiple services to a single machine and verify coordinated port allocation: 81 + 82 + ```bash 83 + # 1. Create machine 84 + tin machine create test-ports prod nas-server 85 + 86 + # 2. Deploy first service (needs 2 ports) 87 + tin service deploy test-ports-prod lldap 88 + 89 + # 3. Deploy second service (needs 2 ports) 90 + tin service deploy test-ports-prod caddy 91 + 92 + # 4. Deploy third service (needs 1 port) 93 + tin service deploy test-ports-prod redis 94 + 95 + # 5. Verify port allocations in central registry 96 + cat /mnt/station-prod/data/machines/topsheet/test-ports-prod/ports 97 + # Expected output: 98 + # lldap:50X00:2 99 + # caddy:50X02:2 100 + # redis:50X04:1 101 + # (where X is machine number) 102 + 103 + # 6. Verify each service has its own .env with allocated ports 104 + cat /mnt/test-ports-prod/service/lldap/.env 105 + # Expected: Contains TIN_PORT_0=50X00, TIN_PORT_1=50X01 106 + 107 + cat /mnt/test-ports-prod/service/caddy/.env 108 + # Expected: Contains TIN_PORT_0=50X02, TIN_PORT_1=50X03 109 + 110 + cat /mnt/test-ports-prod/service/redis/.env 111 + # Expected: Contains TIN_PORT_0=50X04 112 + 113 + # 7. Remove a service and verify port deallocation 114 + tin service rm test-ports-prod caddy 115 + cat /mnt/station-prod/data/machines/topsheet/test-ports-prod/ports 116 + # Expected: caddy entry removed, lldap and redis remain 117 + 118 + # 8. Redeploy service into freed space 119 + tin service deploy test-ports-prod caddy 120 + cat /mnt/station-prod/data/machines/topsheet/test-ports-prod/ports 121 + # Expected: caddy re-allocated (possibly to 50X02 using first-fit) 122 + ``` 123 + 124 + ### Success Criteria 125 + 126 + - [ ] Multiple services can deploy to same machine 127 + - [ ] Port registry tracks all allocations 128 + - [ ] Each service gets unique consecutive ports 129 + - [ ] Service .env files contain correct TIN_PORT_* variables 130 + - [ ] Port deallocation works on service removal 131 + - [ ] First-fit algorithm reuses freed ports 132 + - [ ] Port exhaustion error is clear and actionable 133 + - [ ] No port conflicts between services 75 134 76 135 ## service-env 77 136