diff --git a/tools/pve/oci-deploy.sh b/tools/pve/oci-deploy.sh index 12dfb4ba2..18ab63e2a 100644 --- a/tools/pve/oci-deploy.sh +++ b/tools/pve/oci-deploy.sh @@ -1,344 +1,348 @@ #!/usr/bin/env bash -# Maintainer: MickLesk (CanbiZ) + # Copyright (c) 2021-2025 community-scripts ORG -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# OCI Container Deployment Helper for Proxmox VE 9.1+ - -set -euo pipefail -shopt -s inherit_errexit nullglob - -# Color codes -YW=$(echo "\033[33m") -BL=$(echo "\033[36m") -RD=$(echo "\033[01;31m") -BGN=$(echo "\033[4;92m") -GN=$(echo "\033[1;92m") -DGN=$(echo "\033[32m") -CL=$(echo "\033[m") -BFR="\\r\\033[K" -HOLD="-" -CM="${GN}✓${CL}" -CROSS="${RD}✗${CL}" - -msg_info() { - local msg="$1" - echo -ne " ${HOLD} ${YW}${msg}..." -} - -msg_ok() { - local msg="$1" - echo -e "${BFR} ${CM} ${GN}${msg}${CL}" -} - -msg_error() { - local msg="$1" - echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}" -} - -# Check if running on Proxmox VE 9.1+ -check_proxmox_version() { - if ! command -v pveversion &>/dev/null; then - msg_error "This script must be run on Proxmox VE" - exit 1 - fi - - local pve_version=$(pveversion | grep -oP 'pve-manager/\K[0-9.]+' | cut -d. -f1,2) - local major=$(echo "$pve_version" | cut -d. -f1) - local minor=$(echo "$pve_version" | cut -d. -f2) - - if [[ "$major" -lt 9 ]] || { [[ "$major" -eq 9 ]] && [[ "$minor" -lt 1 ]]; }; then - msg_error "Proxmox VE 9.1 or higher required (current: $pve_version)" - exit 1 - fi -} - -# Parse OCI image reference -parse_image_ref() { - local image_ref="$1" - local registry="" - local image="" - local tag="latest" - - # Handle different formats: - # - nginx:latest - # - docker.io/library/nginx:latest - # - ghcr.io/user/repo:tag - - if [[ "$image_ref" =~ ^([^/]+\.[^/]+)/ ]]; then - # Has registry prefix (contains dot) - registry="${BASH_REMATCH[1]}" - image_ref="${image_ref#*/}" - else - # Use docker.io as default - registry="docker.io" - fi - - # Extract tag if present - if [[ "$image_ref" =~ :([^:]+)$ ]]; then - tag="${BASH_REMATCH[1]}" - image="${image_ref%:*}" - else - image="$image_ref" - fi - - # Add library/ prefix for official docker hub images - if [[ "$registry" == "docker.io" ]] && [[ ! "$image" =~ / ]]; then - image="library/$image" - fi - - echo "$registry/$image:$tag" -} - -# Show usage -usage() { - cat </dev/null; then - msg_error "Failed to create container" - exit 1 - fi - - msg_ok "Container created (ID: $vmid)" - - # Set environment variables if provided - if [[ ${#env_vars[@]} -gt 0 ]]; then - msg_info "Configuring environment variables" - for env_var in "${env_vars[@]}"; do - local key="${env_var%%=*}" - local value="${env_var#*=}" - pct set "$vmid" --env "$key=$value" >/dev/null 2>&1 - done - msg_ok "Environment variables configured (${#env_vars[@]} variables)" - fi - - # Start container if requested - if [[ "$start_after" == "1" ]]; then - msg_info "Starting container" - if pct start "$vmid" >/dev/null 2>&1; then - msg_ok "Container started successfully" - - # Wait for network - sleep 3 - local container_ip=$(pct exec "$vmid" -- hostname -I 2>/dev/null | awk '{print $1}' || echo "N/A") - - echo -e "\n${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" - echo -e "${BL}Container Information:${CL}" - echo -e " ID: ${GN}$vmid${CL}" - echo -e " Name: ${GN}$name${CL}" - echo -e " Image: ${GN}$full_image${CL}" - echo -e " IP: ${GN}$container_ip${CL}" - echo -e " Status: ${GN}Running${CL}" - echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}\n" - else - msg_error "Failed to start container" - fi - else - echo -e "\n${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" - echo -e "${BL}Container Information:${CL}" - echo -e " ID: ${GN}$vmid${CL}" - echo -e " Name: ${GN}$name${CL}" - echo -e " Image: ${GN}$full_image${CL}" - echo -e " Status: ${YW}Stopped${CL}" - echo -e "\n${YW}Start with: pct start $vmid${CL}" - echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}\n" - fi +function msg_info() { + local msg="$1" + echo -e "${INFO} ${YW}${msg}...${CL}" } -# Parse command line arguments -IMAGE="" -NAME="" -VMID="" -CORES="2" -MEMORY="2048" -DISK="8" -STORAGE="" -NETWORK="vmbr0" -IP="dhcp" -GATEWAY="" -PRIVILEGED="0" -START="0" -ENV_VARS=() -VOLUMES=() +function msg_ok() { + local msg="$1" + echo -e "${CM} ${GN}${msg}${CL}" +} -while [[ $# -gt 0 ]]; do - case $1 in - -h | --help) - usage - ;; - -n | --name) - NAME="$2" - shift 2 - ;; - -i | --vmid) - VMID="$2" - shift 2 - ;; - -c | --cores) - CORES="$2" - shift 2 - ;; - -m | --memory) - MEMORY="$2" - shift 2 - ;; - -d | --disk) - DISK="$2" - shift 2 - ;; - -s | --storage) - STORAGE="$2" - shift 2 - ;; - --network) - NETWORK="$2" - shift 2 - ;; - --ip) - IP="$2" - shift 2 - ;; - --gateway) - GATEWAY="$2" - shift 2 - ;; - -e | --env) - ENV_VARS+=("$2") - shift 2 - ;; - -v | --volume) - VOLUMES+=("$2") - shift 2 - ;; - --privileged) - PRIVILEGED="1" - shift - ;; - --start) - START="1" - shift - ;; - -*) - echo "Unknown option: $1" - usage - ;; - *) - IMAGE="$1" - shift - ;; - esac -done +function msg_error() { + local msg="$1" + echo -e "${CROSS} ${RD}${msg}${CL}" +} -# Check if image is provided -if [[ -z "$IMAGE" ]]; then - msg_error "No image specified" - usage +# Check Proxmox version +if ! command -v pveversion &>/dev/null; then + msg_error "This script must be run on Proxmox VE" + exit 1 fi -# Deploy the container -deploy_oci_container "$IMAGE" "$NAME" "$VMID" "$CORES" "$MEMORY" "$DISK" "$STORAGE" "$NETWORK" "$IP" "$GATEWAY" "$PRIVILEGED" "$START" "${ENV_VARS[@]}" +PVE_VER=$(pveversion | grep -oP 'pve-manager/\K[0-9.]+' | cut -d. -f1,2) +MAJOR=$(echo "$PVE_VER" | cut -d. -f1) +MINOR=$(echo "$PVE_VER" | cut -d. -f2) -exit 0 +if [[ "$MAJOR" -lt 9 ]] || { [[ "$MAJOR" -eq 9 ]] && [[ "$MINOR" -lt 1 ]]; }; then + msg_error "Proxmox VE 9.1+ required (current: $PVE_VER)" + exit 1 +fi + +msg_ok "Proxmox VE $PVE_VER detected" + +# Parse OCI image +parse_image() { + local input="$1" + if [[ "$input" =~ ^([^/]+\.[^/]+)/ ]]; then + echo "$input" + elif [[ "$input" =~ / ]]; then + echo "docker.io/$input" + else + echo "docker.io/library/$input" + fi +} + +# Interactive image selection +if [[ -z "${OCI_IMAGE:-}" ]]; then + echo "" + echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e "${BL}Select OCI Image:${CL}" + echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e " ${BL}1)${CL} nginx:alpine - Lightweight web server" + echo -e " ${BL}2)${CL} postgres:16-alpine - PostgreSQL database" + echo -e " ${BL}3)${CL} redis:alpine - Redis cache" + echo -e " ${BL}4)${CL} mariadb:latest - MariaDB database" + echo -e " ${BL}5)${CL} ghcr.io/linkwarden/linkwarden:latest" + echo -e " ${BL}6)${CL} Custom image" + echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo "" + + read -r -p "Select option (1-6): " IMAGE_CHOICE + + case $IMAGE_CHOICE in + 1) OCI_IMAGE="nginx:alpine" ;; + 2) OCI_IMAGE="postgres:16-alpine" ;; + 3) OCI_IMAGE="redis:alpine" ;; + 4) OCI_IMAGE="mariadb:latest" ;; + 5) OCI_IMAGE="ghcr.io/linkwarden/linkwarden:latest" ;; + 6) + read -r -p "Enter OCI image (e.g., ghcr.io/user/repo:tag): " OCI_IMAGE + [[ -z "$OCI_IMAGE" ]] && { msg_error "No image specified"; exit 1; } + ;; + *) + msg_error "Invalid choice" + exit 1 + ;; + esac +fi + +FULL_IMAGE=$(parse_image "$OCI_IMAGE") +msg_ok "Selected: $FULL_IMAGE" + +# Derive container name +if [[ -z "${CT_NAME:-}" ]]; then + DEFAULT_NAME=$(echo "$OCI_IMAGE" | sed 's|.*/||; s/:.*//; s/[^a-zA-Z0-9-]/-/g' | cut -c1-60) + read -r -p "Container name [${DEFAULT_NAME}]: " CT_NAME + CT_NAME=${CT_NAME:-$DEFAULT_NAME} +fi + +# Get next VMID +if [[ -z "${VMID:-}" ]]; then + NEXT_ID=$(pvesh get /cluster/nextid) + read -r -p "Container ID [${NEXT_ID}]: " VMID + VMID=${VMID:-$NEXT_ID} +fi + +# Resources +if [[ -z "${CORES:-}" ]]; then + read -r -p "CPU cores [2]: " CORES + CORES=${CORES:-2} +fi + +if [[ -z "${MEMORY:-}" ]]; then + read -r -p "Memory in MB [2048]: " MEMORY + MEMORY=${MEMORY:-2048} +fi + +if [[ -z "${DISK:-}" ]]; then + read -r -p "Disk size in GB [8]: " DISK + DISK=${DISK:-8} +fi + +# Storage +if [[ -z "${STORAGE:-}" ]]; then + AVAIL_STORAGE=$(pvesm status | awk '/^local-(zfs|lvm)/ {print $1; exit}') + [[ -z "$AVAIL_STORAGE" ]] && AVAIL_STORAGE="local" + read -r -p "Storage [${AVAIL_STORAGE}]: " STORAGE + STORAGE=${STORAGE:-$AVAIL_STORAGE} +fi + +# Network +if [[ -z "${BRIDGE:-}" ]]; then + read -r -p "Network bridge [vmbr0]: " BRIDGE + BRIDGE=${BRIDGE:-vmbr0} +fi + +if [[ -z "${IP_MODE:-}" ]]; then + read -r -p "IP mode (dhcp/static) [dhcp]: " IP_MODE + IP_MODE=${IP_MODE:-dhcp} +fi + +if [[ "$IP_MODE" == "static" ]]; then + read -r -p "Static IP (CIDR, e.g., 192.168.1.100/24): " STATIC_IP + read -r -p "Gateway IP: " GATEWAY +fi + +# Environment variables +declare -a ENV_VARS=() + +case "$OCI_IMAGE" in + postgres*|postgresql*) + echo "" + msg_info "PostgreSQL requires environment variables" + read -r -p "PostgreSQL password: " -s PG_PASS + echo "" + ENV_VARS+=("POSTGRES_PASSWORD=$PG_PASS") + + read -r -p "Create database (optional): " PG_DB + [[ -n "$PG_DB" ]] && ENV_VARS+=("POSTGRES_DB=$PG_DB") + + read -r -p "PostgreSQL user (optional): " PG_USER + [[ -n "$PG_USER" ]] && ENV_VARS+=("POSTGRES_USER=$PG_USER") + ;; + + mariadb*|mysql*) + echo "" + msg_info "MariaDB/MySQL requires environment variables" + read -r -p "Root password: " -s MYSQL_PASS + echo "" + ENV_VARS+=("MYSQL_ROOT_PASSWORD=$MYSQL_PASS") + + read -r -p "Create database (optional): " MYSQL_DB + [[ -n "$MYSQL_DB" ]] && ENV_VARS+=("MYSQL_DATABASE=$MYSQL_DB") + + read -r -p "Create user (optional): " MYSQL_USER + if [[ -n "$MYSQL_USER" ]]; then + ENV_VARS+=("MYSQL_USER=$MYSQL_USER") + read -r -p "User password: " -s MYSQL_USER_PASS + echo "" + ENV_VARS+=("MYSQL_PASSWORD=$MYSQL_USER_PASS") + fi + ;; + + *linkwarden*) + echo "" + msg_info "Linkwarden configuration" + read -r -p "NEXTAUTH_SECRET (press Enter to generate): " NEXTAUTH_SECRET + if [[ -z "$NEXTAUTH_SECRET" ]]; then + NEXTAUTH_SECRET=$(openssl rand -base64 32) + fi + ENV_VARS+=("NEXTAUTH_SECRET=$NEXTAUTH_SECRET") + + read -r -p "NEXTAUTH_URL [http://localhost:3000]: " NEXTAUTH_URL + NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000} + ENV_VARS+=("NEXTAUTH_URL=$NEXTAUTH_URL") + + read -r -p "DATABASE_URL (PostgreSQL connection string): " DATABASE_URL + [[ -n "$DATABASE_URL" ]] && ENV_VARS+=("DATABASE_URL=$DATABASE_URL") + ;; +esac + +# Additional env vars +read -r -p "Add custom environment variables? (y/N): " ADD_ENV +if [[ "${ADD_ENV,,}" =~ ^(y|yes)$ ]]; then + while true; do + read -r -p "Enter KEY=VALUE (or press Enter to finish): " CUSTOM_ENV + [[ -z "$CUSTOM_ENV" ]] && break + ENV_VARS+=("$CUSTOM_ENV") + done +fi + +# Privileged mode +read -r -p "Run as privileged container? (y/N): " PRIV_MODE +if [[ "${PRIV_MODE,,}" =~ ^(y|yes)$ ]]; then + UNPRIVILEGED="0" +else + UNPRIVILEGED="1" +fi + +# Auto-start +read -r -p "Start container after creation? (Y/n): " AUTO_START +if [[ "${AUTO_START,,}" =~ ^(n|no)$ ]]; then + START_AFTER="no" +else + START_AFTER="yes" +fi + +# Summary +echo "" +echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" +echo -e "${BL}Container Configuration Summary:${CL}" +echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" +echo -e " Image: $FULL_IMAGE" +echo -e " ID: $VMID" +echo -e " Name: $CT_NAME" +echo -e " CPUs: $CORES" +echo -e " Memory: ${MEMORY}MB" +echo -e " Disk: ${DISK}GB" +echo -e " Storage: $STORAGE" +echo -e " Network: $BRIDGE ($IP_MODE)" +[[ ${#ENV_VARS[@]} -gt 0 ]] && echo -e " Env vars: ${#ENV_VARS[@]} configured" +echo -e "${YW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" +echo "" + +read -r -p "Proceed with creation? (Y/n): " CONFIRM +if [[ "${CONFIRM,,}" =~ ^(n|no)$ ]]; then + msg_error "Cancelled by user" + exit 0 +fi + +# Create container +msg_info "Creating container $VMID" + +PCT_CMD="pct create $VMID oci://$FULL_IMAGE" +PCT_CMD+=" --hostname $CT_NAME" +PCT_CMD+=" --cores $CORES" +PCT_CMD+=" --memory $MEMORY" +PCT_CMD+=" --rootfs ${STORAGE}:${DISK}" +PCT_CMD+=" --unprivileged $UNPRIVILEGED" + +if [[ "$IP_MODE" == "static" && -n "$STATIC_IP" ]]; then + PCT_CMD+=" --net0 name=eth0,bridge=$BRIDGE,ip=$STATIC_IP" + [[ -n "$GATEWAY" ]] && PCT_CMD+=",gw=$GATEWAY" +else + PCT_CMD+=" --net0 name=eth0,bridge=$BRIDGE,ip=dhcp" +fi + +if eval "$PCT_CMD" 2>&1; then + msg_ok "Container created" +else + msg_error "Failed to create container" + exit 1 +fi + +# Set environment variables +if [[ ${#ENV_VARS[@]} -gt 0 ]]; then + msg_info "Configuring environment variables" + for env_var in "${ENV_VARS[@]}"; do + if pct set "$VMID" -env "$env_var" &>/dev/null; then + : + else + msg_error "Failed to set: $env_var" + fi + done + msg_ok "Environment variables configured (${#ENV_VARS[@]} variables)" +fi + +# Start container +if [[ "$START_AFTER" == "yes" ]]; then + msg_info "Starting container" + if pct start "$VMID" 2>&1; then + msg_ok "Container started" + + # Wait for network + sleep 3 + CT_IP=$(pct exec "$VMID" -- hostname -I 2>/dev/null | awk '{print $1}' || echo "N/A") + + echo "" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e "${BL}Container Information:${CL}" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e " ID: ${GN}$VMID${CL}" + echo -e " Name: ${GN}$CT_NAME${CL}" + echo -e " Image: ${GN}$FULL_IMAGE${CL}" + echo -e " IP: ${GN}$CT_IP${CL}" + echo -e " Status: ${GN}Running${CL}" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo "" + echo -e "${INFO} ${YW}Access console:${CL} pct console $VMID" + echo -e "${INFO} ${YW}View logs:${CL} pct logs $VMID" + echo "" + else + msg_error "Failed to start container" + fi +else + echo "" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e "${BL}Container Information:${CL}" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo -e " ID: ${GN}$VMID${CL}" + echo -e " Name: ${GN}$CT_NAME${CL}" + echo -e " Image: ${GN}$FULL_IMAGE${CL}" + echo -e " Status: ${YW}Stopped${CL}" + echo -e "${GN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${CL}" + echo "" + echo -e "${INFO} ${YW}Start with:${CL} pct start $VMID" + echo "" +fi