Initial fleet-api skill
This commit is contained in:
137
lib/http.sh
Normal file
137
lib/http.sh
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/bin/bash
|
||||
# Minimal HTTP server using bash and socat/nc
|
||||
|
||||
# Send HTTP response
|
||||
send_response() {
|
||||
local status="$1"
|
||||
local content_type="${2:-application/json}"
|
||||
local body="$3"
|
||||
|
||||
local length=${#body}
|
||||
|
||||
printf "HTTP/1.1 %s\r\n" "$status"
|
||||
printf "Content-Type: %s\r\n" "$content_type"
|
||||
printf "Content-Length: %d\r\n" "$length"
|
||||
printf "Access-Control-Allow-Origin: *\r\n"
|
||||
printf "Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS\r\n"
|
||||
printf "Access-Control-Allow-Headers: Content-Type\r\n"
|
||||
printf "Connection: close\r\n"
|
||||
printf "\r\n"
|
||||
printf "%s" "$body"
|
||||
}
|
||||
|
||||
send_json() {
|
||||
send_response "$1" "application/json" "$2"
|
||||
}
|
||||
|
||||
send_error() {
|
||||
local status="$1"
|
||||
local message="$2"
|
||||
send_json "$status" "{\"error\":\"$message\"}"
|
||||
}
|
||||
|
||||
# Handle a single HTTP request
|
||||
handle_request() {
|
||||
local request_line=""
|
||||
local content_length=0
|
||||
local body=""
|
||||
|
||||
# Read request line
|
||||
read -r request_line
|
||||
request_line=$(echo "$request_line" | tr -d '\r')
|
||||
|
||||
# Parse method and path
|
||||
local method=$(echo "$request_line" | cut -d' ' -f1)
|
||||
local path=$(echo "$request_line" | cut -d' ' -f2)
|
||||
|
||||
# Read headers
|
||||
while read -r header; do
|
||||
header=$(echo "$header" | tr -d '\r')
|
||||
[ -z "$header" ] && break
|
||||
|
||||
# Extract content-length
|
||||
if echo "$header" | grep -qi "^content-length:"; then
|
||||
content_length=$(echo "$header" | cut -d':' -f2 | tr -d ' ')
|
||||
fi
|
||||
done
|
||||
|
||||
# Read body if present
|
||||
if [ "$content_length" -gt 0 ] 2>/dev/null; then
|
||||
body=$(head -c "$content_length")
|
||||
fi
|
||||
|
||||
# Route request
|
||||
route_request "$method" "$path" "$body"
|
||||
}
|
||||
|
||||
# Route requests to handlers
|
||||
route_request() {
|
||||
local method="$1"
|
||||
local path="$2"
|
||||
local body="$3"
|
||||
|
||||
# Handle CORS preflight
|
||||
if [ "$method" = "OPTIONS" ]; then
|
||||
send_response "204 No Content" "text/plain" ""
|
||||
return
|
||||
fi
|
||||
|
||||
# API routes
|
||||
case "$path" in
|
||||
/api/machines/register)
|
||||
if [ "$method" = "POST" ]; then
|
||||
handle_register "$body"
|
||||
else
|
||||
send_error "405 Method Not Allowed" "Use POST"
|
||||
fi
|
||||
;;
|
||||
/api/machines)
|
||||
if [ "$method" = "GET" ]; then
|
||||
handle_list_machines
|
||||
else
|
||||
send_error "405 Method Not Allowed" "Use GET"
|
||||
fi
|
||||
;;
|
||||
/api/machines/*)
|
||||
local machine_id="${path#/api/machines/}"
|
||||
if [ "$method" = "DELETE" ]; then
|
||||
handle_delete_machine "$machine_id"
|
||||
elif [ "$method" = "GET" ]; then
|
||||
handle_get_machine "$machine_id"
|
||||
else
|
||||
send_error "405 Method Not Allowed" "Use GET or DELETE"
|
||||
fi
|
||||
;;
|
||||
/health)
|
||||
send_json "200 OK" '{"status":"healthy"}'
|
||||
;;
|
||||
*)
|
||||
send_error "404 Not Found" "Endpoint not found"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Start HTTP server
|
||||
http_server() {
|
||||
local port="$1"
|
||||
|
||||
# Check for socat
|
||||
if command -v socat &>/dev/null; then
|
||||
echo "Using socat..."
|
||||
while true; do
|
||||
socat TCP-LISTEN:"$port",reuseaddr,fork EXEC:"$0 --handle-request"
|
||||
done
|
||||
else
|
||||
echo "Error: socat not found. Install with: apt-get install socat"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Handle --handle-request flag (called by socat)
|
||||
if [ "$1" = "--handle-request" ]; then
|
||||
SKILL_DIR="$(dirname "$(dirname "$0")")"
|
||||
source "$SKILL_DIR/lib/machines.sh"
|
||||
DATA_DIR="${FLEET_DATA_DIR:-/data/fleet}"
|
||||
export DATA_DIR
|
||||
handle_request
|
||||
fi
|
||||
131
lib/machines.sh
Normal file
131
lib/machines.sh
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
# Machine registry logic
|
||||
|
||||
MACHINES_FILE="${DATA_DIR:-/data/fleet}/machines.json"
|
||||
STALE_THRESHOLD=60 # seconds
|
||||
|
||||
# Get current timestamp
|
||||
now() {
|
||||
date +%s
|
||||
}
|
||||
|
||||
# Read machines file with lock
|
||||
read_machines() {
|
||||
if [ -f "$MACHINES_FILE" ]; then
|
||||
cat "$MACHINES_FILE"
|
||||
else
|
||||
echo '{}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Write machines file with lock
|
||||
write_machines() {
|
||||
local data="$1"
|
||||
echo "$data" > "$MACHINES_FILE.tmp"
|
||||
mv "$MACHINES_FILE.tmp" "$MACHINES_FILE"
|
||||
}
|
||||
|
||||
# Update machine status based on lastSeen
|
||||
update_status() {
|
||||
local machines="$1"
|
||||
local current_time=$(now)
|
||||
|
||||
echo "$machines" | jq --arg now "$current_time" --arg threshold "$STALE_THRESHOLD" '
|
||||
to_entries | map(
|
||||
.value.status = (
|
||||
if (.value.lastSeen | tonumber) < (($now | tonumber) - ($threshold | tonumber))
|
||||
then "unknown"
|
||||
else .value.status
|
||||
end
|
||||
)
|
||||
) | from_entries
|
||||
'
|
||||
}
|
||||
|
||||
# Handle POST /api/machines/register
|
||||
handle_register() {
|
||||
local body="$1"
|
||||
|
||||
# Validate JSON
|
||||
if ! echo "$body" | jq -e . >/dev/null 2>&1; then
|
||||
send_error "400 Bad Request" "Invalid JSON"
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract required fields
|
||||
local id=$(echo "$body" | jq -r '.id // empty')
|
||||
local name=$(echo "$body" | jq -r '.name // empty')
|
||||
local url=$(echo "$body" | jq -r '.url // empty')
|
||||
|
||||
if [ -z "$id" ] || [ -z "$name" ] || [ -z "$url" ]; then
|
||||
send_error "400 Bad Request" "Missing required fields: id, name, url"
|
||||
return
|
||||
fi
|
||||
|
||||
# Read current machines
|
||||
local machines=$(read_machines)
|
||||
local current_time=$(now)
|
||||
|
||||
# Create/update machine entry
|
||||
local machine=$(echo "$body" | jq --arg ts "$current_time" '. + {lastSeen: ($ts | tonumber)}')
|
||||
|
||||
# Add to machines
|
||||
machines=$(echo "$machines" | jq --arg id "$id" --argjson machine "$machine" '.[$id] = $machine')
|
||||
|
||||
# Write back
|
||||
write_machines "$machines"
|
||||
|
||||
send_json "200 OK" "$machine"
|
||||
}
|
||||
|
||||
# Handle GET /api/machines
|
||||
handle_list_machines() {
|
||||
local machines=$(read_machines)
|
||||
|
||||
# Update status based on lastSeen
|
||||
machines=$(update_status "$machines")
|
||||
|
||||
# Convert to array
|
||||
local result=$(echo "$machines" | jq '[to_entries[] | .value]')
|
||||
|
||||
send_json "200 OK" "$result"
|
||||
}
|
||||
|
||||
# Handle GET /api/machines/:id
|
||||
handle_get_machine() {
|
||||
local id="$1"
|
||||
local machines=$(read_machines)
|
||||
|
||||
# Update status
|
||||
machines=$(update_status "$machines")
|
||||
|
||||
# Get machine
|
||||
local machine=$(echo "$machines" | jq --arg id "$id" '.[$id] // null')
|
||||
|
||||
if [ "$machine" = "null" ]; then
|
||||
send_error "404 Not Found" "Machine not found"
|
||||
return
|
||||
fi
|
||||
|
||||
send_json "200 OK" "$machine"
|
||||
}
|
||||
|
||||
# Handle DELETE /api/machines/:id
|
||||
handle_delete_machine() {
|
||||
local id="$1"
|
||||
local machines=$(read_machines)
|
||||
|
||||
# Check if exists
|
||||
local exists=$(echo "$machines" | jq --arg id "$id" 'has($id)')
|
||||
|
||||
if [ "$exists" = "false" ]; then
|
||||
send_error "404 Not Found" "Machine not found"
|
||||
return
|
||||
fi
|
||||
|
||||
# Remove machine
|
||||
machines=$(echo "$machines" | jq --arg id "$id" 'del(.[$id])')
|
||||
write_machines "$machines"
|
||||
|
||||
send_json "200 OK" '{"deleted":true}'
|
||||
}
|
||||
Reference in New Issue
Block a user