#!/bin/sh # RezApp Forge - Docker to LXC Converter # Copyright (C) 2026 SecuBox . /lib/functions.sh CONFIG="rezapp" CACHE_DIR="" OUTPUT_DIR="" APPS_DIR="" LXC_DIR="" DEFAULT_MEMORY="" DEFAULT_NETWORK="" TEMPLATES_DIR="/usr/share/rezapp/templates" # Logging log_info() { echo "[INFO] $*"; } log_warn() { echo "[WARN] $*" >&2; } log_error() { echo "[ERROR] $*" >&2; } # Container runtime (docker or podman) CONTAINER_RUNTIME="" # Detect and initialize container runtime (Docker or Podman fallback) init_runtime() { [ -n "$CONTAINER_RUNTIME" ] && return 0 # Try Docker first if command -v docker >/dev/null 2>&1; then if docker info >/dev/null 2>&1; then CONTAINER_RUNTIME="docker" log_info "Using Docker runtime" return 0 fi # Try starting Docker daemon if [ -x /etc/init.d/dockerd ]; then log_info "Starting Docker daemon..." /etc/init.d/dockerd start 2>/dev/null sleep 5 if docker info >/dev/null 2>&1; then CONTAINER_RUNTIME="docker" log_info "Using Docker runtime" return 0 fi fi fi # Fallback to Podman if command -v podman >/dev/null 2>&1; then if podman info >/dev/null 2>&1; then CONTAINER_RUNTIME="podman" log_info "Using Podman runtime (fallback)" return 0 fi fi log_error "No container runtime available (docker or podman)" return 1 } # Runtime wrapper functions runtime_pull() { $CONTAINER_RUNTIME pull "$@" } runtime_create() { $CONTAINER_RUNTIME create "$@" } runtime_export() { $CONTAINER_RUNTIME export "$@" } runtime_rm() { $CONTAINER_RUNTIME rm "$@" } runtime_inspect() { $CONTAINER_RUNTIME inspect "$@" } # Load configuration load_config() { config_load "$CONFIG" config_get CACHE_DIR main cache_dir "/srv/rezapp/cache" config_get OUTPUT_DIR main output_dir "/srv/rezapp/generated" config_get APPS_DIR main apps_dir "/srv/rezapp/apps" config_get LXC_DIR main lxc_dir "/srv/lxc" config_get DEFAULT_MEMORY main default_memory "512M" config_get DEFAULT_NETWORK main default_network "host" } # Ensure directories exist ensure_dirs() { mkdir -p "$CACHE_DIR" "$OUTPUT_DIR" "$APPS_DIR" } # ========================================== # Catalog Commands # ========================================== cmd_catalog_list() { echo "Enabled Catalogs:" echo "=================" _print_catalog() { local section="$1" local name enabled type namespace config_get name "$section" name "$section" config_get enabled "$section" enabled "0" config_get type "$section" type "dockerhub" config_get namespace "$section" namespace "" [ "$enabled" = "1" ] || return if [ -n "$namespace" ]; then printf " %-15s %-12s %s\n" "$section" "$type" "$namespace/*" else printf " %-15s %-12s (all)\n" "$section" "$type" fi } config_load "$CONFIG" config_foreach _print_catalog catalog } cmd_catalog_add() { local name="$1" local namespace="$2" [ -z "$name" ] && { log_error "Catalog name required"; return 1; } uci set rezapp.$name=catalog uci set rezapp.$name.name="$name" uci set rezapp.$name.type="dockerhub" [ -n "$namespace" ] && uci set rezapp.$name.namespace="$namespace" uci set rezapp.$name.enabled="1" uci commit rezapp log_info "Catalog added: $name" } cmd_catalog_remove() { local name="$1" [ -z "$name" ] && { log_error "Catalog name required"; return 1; } uci delete rezapp.$name 2>/dev/null uci commit rezapp log_info "Catalog removed: $name" } # ========================================== # Search Commands # ========================================== cmd_search() { local query="$1" local catalog="$2" [ -z "$query" ] && { log_error "Search query required"; return 1; } log_info "Searching Docker Hub for: $query" local url="https://hub.docker.com/v2/search/repositories/?query=${query}&page_size=25" local result=$(curl -s "$url") if [ -z "$result" ]; then log_error "Search failed" return 1 fi echo "" echo "Search Results:" echo "===============" echo "$result" | jsonfilter -e '@.results[*]' | while read -r item; do local name=$(echo "$item" | jsonfilter -e '@.repo_name') local desc=$(echo "$item" | jsonfilter -e '@.short_description' | head -c 60) local stars=$(echo "$item" | jsonfilter -e '@.star_count') local official=$(echo "$item" | jsonfilter -e '@.is_official') local badge="" [ "$official" = "true" ] && badge="[official]" printf " %-35s %5s* %s %s\n" "$name" "$stars" "$badge" "$desc" done } cmd_info() { local image="$1" [ -z "$image" ] && { log_error "Image name required"; return 1; } # Parse image name local namespace="${image%%/*}" local repo="${image#*/}" if [ "$namespace" = "$image" ]; then namespace="library" repo="$image" fi log_info "Fetching info for: $image" # Get tags local tags_url="https://hub.docker.com/v2/repositories/${namespace}/${repo}/tags?page_size=10" local tags=$(curl -s "$tags_url") echo "" echo "Image: $image" echo "========================================" echo "" echo "Available Tags:" echo "$tags" | jsonfilter -e '@.results[*].name' | head -10 | while read tag; do echo " - $tag" done # Try to get config for latest echo "" echo "To convert: rezappctl convert $image --name " } # ========================================== # Convert Command (with offline mode support) # ========================================== cmd_convert() { local image="" local name="" local tag="latest" local memory="" local network="" local ports="" local mounts="" local from_tar="" local from_oci="" local offline="0" # Parse arguments while [ $# -gt 0 ]; do case "$1" in --name) name="$2"; shift 2 ;; --tag) tag="$2"; shift 2 ;; --memory) memory="$2"; shift 2 ;; --network) network="$2"; shift 2 ;; --port) ports="$ports $2"; shift 2 ;; --mount) mounts="$mounts $2"; shift 2 ;; --from-tar) from_tar="$2"; offline="1"; shift 2 ;; --from-oci) from_oci="$2"; offline="1"; shift 2 ;; --offline) offline="1"; shift ;; -*) log_error "Unknown option: $1"; return 1 ;; *) image="$1"; shift ;; esac done # Offline mode: convert from local tarball if [ -n "$from_tar" ]; then [ ! -f "$from_tar" ] && { log_error "Tarball not found: $from_tar"; return 1; } [ -z "$name" ] && { log_error "Name required with --from-tar"; return 1; } _convert_from_tarball "$from_tar" "$name" "$memory" "$network" return $? fi # Offline mode: convert from OCI directory if [ -n "$from_oci" ]; then [ ! -d "$from_oci" ] && { log_error "OCI directory not found: $from_oci"; return 1; } [ -z "$name" ] && { log_error "Name required with --from-oci"; return 1; } _convert_from_oci "$from_oci" "$name" "$memory" "$network" return $? fi # Check cache for existing tarball (offline conversion) [ -z "$image" ] && { log_error "Image name required"; return 1; } # Default name from image if [ -z "$name" ]; then name="${image##*/}" name="${name%%:*}" fi [ -z "$memory" ] && memory="$DEFAULT_MEMORY" [ -z "$network" ] && network="$DEFAULT_NETWORK" local full_image="${image}:${tag}" local app_dir="$APPS_DIR/$name" local lxc_path="$LXC_DIR/$name" local tarball="$CACHE_DIR/${name}.tar" # Check if cached tarball exists if [ -f "$tarball" ] && [ "$offline" = "1" ]; then log_info "Using cached tarball: $tarball" _convert_from_tarball "$tarball" "$name" "$memory" "$network" return $? fi log_info "Converting $full_image -> $name" # Step 1: Initialize container runtime (Docker or Podman) init_runtime || return 1 # Step 2: Pull image log_info "Pulling image via $CONTAINER_RUNTIME..." runtime_pull "$full_image" || { log_error "Failed to pull image"; return 1; } # Step 3: Inspect image log_info "Inspecting image metadata..." mkdir -p "$app_dir" runtime_inspect "$full_image" > "$app_dir/docker-inspect.json" # Extract metadata local entrypoint=$(jsonfilter -i "$app_dir/docker-inspect.json" -e '@[0].Config.Entrypoint[*]' 2>/dev/null | tr '\n' ' ') local cmd=$(jsonfilter -i "$app_dir/docker-inspect.json" -e '@[0].Config.Cmd[*]' 2>/dev/null | tr '\n' ' ') local workdir=$(jsonfilter -i "$app_dir/docker-inspect.json" -e '@[0].Config.WorkingDir' 2>/dev/null) local user=$(jsonfilter -i "$app_dir/docker-inspect.json" -e '@[0].Config.User' 2>/dev/null) local exposed=$(jsonfilter -i "$app_dir/docker-inspect.json" -e '@[0].Config.ExposedPorts' 2>/dev/null) # Extract environment variables local env_vars="" local env_list=$(jsonfilter -i "$app_dir/docker-inspect.json" -e '@[0].Config.Env[*]' 2>/dev/null) if [ -n "$env_list" ]; then # Filter out PATH and other system vars, keep useful ones env_vars=$(echo "$env_list" | grep -vE '^(PATH=|HOME=|HOSTNAME=|TERM=)' | tr '\n' '|') fi log_info "Extracted: entrypoint='$entrypoint' cmd='$cmd' workdir='$workdir' user='$user'" [ -n "$exposed" ] && log_info "Exposed ports: $exposed" # Step 4: Export filesystem log_info "Exporting container filesystem..." runtime_create --name rezapp-export-$$ "$full_image" >/dev/null 2>&1 runtime_export rezapp-export-$$ > "$tarball" runtime_rm rezapp-export-$$ >/dev/null 2>&1 # Create LXC from tarball _create_lxc_from_tar "$tarball" "$name" "$memory" "$entrypoint" "$cmd" "$workdir" "$user" "$full_image" "$network" "$ports" "$env_vars" "$exposed" } # Convert from local tarball (offline mode) _convert_from_tarball() { local tarball="$1" local name="$2" local memory="${3:-$DEFAULT_MEMORY}" local network="${4:-$DEFAULT_NETWORK}" log_info "Converting from tarball: $tarball -> $name" # Try to extract metadata from tarball local entrypoint="" local cmd="/bin/sh" local workdir="/" local user="" # Check for OCI manifest in tarball if tar tf "$tarball" 2>/dev/null | grep -q "manifest.json"; then log_info "Found OCI manifest, extracting metadata..." local manifest_tmp="/tmp/rezapp-manifest-$$.json" tar xf "$tarball" -O manifest.json > "$manifest_tmp" 2>/dev/null # Extract config digest and parse local config_file=$(jsonfilter -i "$manifest_tmp" -e '@[0].Config' 2>/dev/null) if [ -n "$config_file" ] && tar tf "$tarball" 2>/dev/null | grep -q "$config_file"; then tar xf "$tarball" -O "$config_file" > "/tmp/rezapp-config-$$.json" 2>/dev/null entrypoint=$(jsonfilter -i "/tmp/rezapp-config-$$.json" -e '@.config.Entrypoint[*]' 2>/dev/null | tr '\n' ' ') cmd=$(jsonfilter -i "/tmp/rezapp-config-$$.json" -e '@.config.Cmd[*]' 2>/dev/null | tr '\n' ' ') workdir=$(jsonfilter -i "/tmp/rezapp-config-$$.json" -e '@.config.WorkingDir' 2>/dev/null) user=$(jsonfilter -i "/tmp/rezapp-config-$$.json" -e '@.config.User' 2>/dev/null) rm -f "/tmp/rezapp-config-$$.json" fi rm -f "$manifest_tmp" fi _create_lxc_from_tar "$tarball" "$name" "$memory" "$entrypoint" "$cmd" "$workdir" "$user" "local:$tarball" "$network" "" "" "" } # Convert from OCI directory (offline mode) _convert_from_oci() { local oci_dir="$1" local name="$2" local memory="${3:-$DEFAULT_MEMORY}" local network="${4:-$DEFAULT_NETWORK}" log_info "Converting from OCI directory: $oci_dir -> $name" local app_dir="$APPS_DIR/$name" local lxc_path="$LXC_DIR/$name" # Parse OCI index and config local index_file="$oci_dir/index.json" [ ! -f "$index_file" ] && { log_error "OCI index.json not found"; return 1; } local manifest_digest=$(jsonfilter -i "$index_file" -e '@.manifests[0].digest' 2>/dev/null) manifest_digest="${manifest_digest#sha256:}" local manifest_file="$oci_dir/blobs/sha256/$manifest_digest" [ ! -f "$manifest_file" ] && { log_error "OCI manifest not found"; return 1; } # Get config local config_digest=$(jsonfilter -i "$manifest_file" -e '@.config.digest' 2>/dev/null) config_digest="${config_digest#sha256:}" local config_file="$oci_dir/blobs/sha256/$config_digest" local entrypoint="" local cmd="/bin/sh" local workdir="/" local user="" if [ -f "$config_file" ]; then entrypoint=$(jsonfilter -i "$config_file" -e '@.config.Entrypoint[*]' 2>/dev/null | tr '\n' ' ') cmd=$(jsonfilter -i "$config_file" -e '@.config.Cmd[*]' 2>/dev/null | tr '\n' ' ') workdir=$(jsonfilter -i "$config_file" -e '@.config.WorkingDir' 2>/dev/null) user=$(jsonfilter -i "$config_file" -e '@.config.User' 2>/dev/null) fi # Extract layers mkdir -p "$app_dir" "$lxc_path/rootfs" log_info "Extracting OCI layers..." # Get layer digests and extract in order local layers=$(jsonfilter -i "$manifest_file" -e '@.layers[*].digest' 2>/dev/null) for layer_digest in $layers; do layer_digest="${layer_digest#sha256:}" local layer_file="$oci_dir/blobs/sha256/$layer_digest" if [ -f "$layer_file" ]; then log_info " Extracting layer: ${layer_digest:0:12}..." tar xf "$layer_file" -C "$lxc_path/rootfs" 2>/dev/null fi done # Generate LXC config _generate_lxc_config "$name" "$memory" "$entrypoint" "$cmd" "$workdir" "$user" "oci:$oci_dir" "$network" "" "" "" } # Create LXC container from tarball _create_lxc_from_tar() { local tarball="$1" local name="$2" local memory="$3" local entrypoint="$4" local cmd="$5" local workdir="$6" local user="$7" local source="$8" local network="$9" local ports="${10}" local env_vars="${11}" local exposed="${12}" local app_dir="$APPS_DIR/$name" local lxc_path="$LXC_DIR/$name" # Create LXC rootfs log_info "Creating LXC container rootfs..." rm -rf "$lxc_path" mkdir -p "$lxc_path/rootfs" "$app_dir" log_info "Extracting filesystem (this may take a while)..." tar xf "$tarball" -C "$lxc_path/rootfs" 2>/dev/null # Ensure /bin/sh exists (some images use busybox or ash) if [ ! -e "$lxc_path/rootfs/bin/sh" ]; then if [ -e "$lxc_path/rootfs/bin/bash" ]; then ln -sf bash "$lxc_path/rootfs/bin/sh" elif [ -e "$lxc_path/rootfs/bin/busybox" ]; then ln -sf busybox "$lxc_path/rootfs/bin/sh" fi fi _generate_lxc_config "$name" "$memory" "$entrypoint" "$cmd" "$workdir" "$user" "$source" "$network" "$ports" "$env_vars" "$exposed" } # Generate LXC config and metadata _generate_lxc_config() { local name="$1" local memory="$2" local entrypoint="$3" local cmd="$4" local workdir="$5" local user="$6" local source="$7" local network="$8" local ports="$9" local env_vars="${10}" local exposed_ports="${11}" local app_dir="$APPS_DIR/$name" local lxc_path="$LXC_DIR/$name" # Generate start script log_info "Generating start script..." local start_script="$lxc_path/rootfs/start-lxc.sh" # Build the exec command local exec_cmd="" if [ -n "$entrypoint" ]; then exec_cmd="$entrypoint" [ -n "$cmd" ] && exec_cmd="$exec_cmd $cmd" elif [ -n "$cmd" ]; then exec_cmd="$cmd" else exec_cmd="/bin/sh" fi cat > "$start_script" << STARTEOF #!/bin/sh # Auto-generated by RezApp Forge from $source # Set working directory cd ${workdir:-/} # Create common directories mkdir -p /config /data /tmp 2>/dev/null # Export environment export HOME=\${HOME:-/root} export PATH=\${PATH:-/usr/local/bin:/usr/bin:/bin} # Run entrypoint/cmd exec $exec_cmd STARTEOF chmod +x "$start_script" # Parse user for UID/GID local uid="0" local gid="0" if [ -n "$user" ] && [ "$user" != "root" ]; then # Handle numeric or name:name format case "$user" in *:*) uid="${user%%:*}" gid="${user#*:}" ;; [0-9]*) uid="$user" gid="$user" ;; *) # Named user - keep as 0 for now uid="0" gid="0" ;; esac fi # Convert memory to bytes local mem_bytes case "$memory" in *G) mem_bytes=$(( ${memory%G} * 1073741824 )) ;; *M) mem_bytes=$(( ${memory%M} * 1048576 )) ;; *K) mem_bytes=$(( ${memory%K} * 1024 )) ;; *) mem_bytes="$memory" ;; esac log_info "Generating LXC config (network: $network)..." # Start LXC config cat > "$lxc_path/config" << LXCEOF # LXC config for $name # Auto-generated by RezApp Forge from $source lxc.uts.name = $name lxc.rootfs.path = dir:$lxc_path/rootfs lxc.init.cmd = /start-lxc.sh # Filesystem mounts lxc.mount.auto = proc:mixed sys:ro lxc.mount.entry = /srv/$name data none bind,create=dir 0 0 # Resource limits lxc.cgroup2.memory.max = $mem_bytes # User/Group lxc.init.uid = $uid lxc.init.gid = $gid # Drop dangerous capabilities lxc.cap.drop = sys_admin sys_module sys_boot sys_rawio mac_admin mac_override # TTY configuration lxc.console.size = 1024 lxc.pty.max = 1024 lxc.tty.max = 4 # Device access lxc.cgroup2.devices.allow = c 1:* rwm lxc.cgroup2.devices.allow = c 5:* rwm lxc.cgroup2.devices.allow = c 136:* rwm # Disable seccomp for compatibility lxc.seccomp.profile = # Autostart disabled by default lxc.start.auto = 0 LXCEOF # Add network configuration based on mode case "$network" in host) cat >> "$lxc_path/config" << NETEOF # Network: Share host namespace lxc.namespace.share.net = 1 NETEOF ;; bridge) cat >> "$lxc_path/config" << NETEOF # Network: Bridge mode lxc.net.0.type = veth lxc.net.0.link = br-lan lxc.net.0.flags = up lxc.net.0.name = eth0 NETEOF ;; none) cat >> "$lxc_path/config" << NETEOF # Network: None (isolated) lxc.net.0.type = none NETEOF ;; *) # Default to host network for simplicity cat >> "$lxc_path/config" << NETEOF # Network: Share host namespace (default) lxc.namespace.share.net = 1 NETEOF ;; esac # Add environment variables cat >> "$lxc_path/config" << ENVEOF # Environment variables lxc.environment = PUID=$uid lxc.environment = PGID=$gid lxc.environment = TZ=Europe/Paris ENVEOF # Add extracted Docker ENV vars if [ -n "$env_vars" ]; then echo "" >> "$lxc_path/config" echo "# Docker ENV defaults" >> "$lxc_path/config" echo "$env_vars" | tr '|' '\n' | while read -r env; do [ -n "$env" ] && echo "lxc.environment = $env" >> "$lxc_path/config" done fi # Create data directory mkdir -p "/srv/$name" [ "$uid" != "0" ] && chown "$uid:$gid" "/srv/$name" 2>/dev/null # Auto-detect ports from Docker EXPOSE local detected_ports="" if [ -n "$exposed_ports" ]; then detected_ports=$(echo "$exposed_ports" | sed 's|[{}"]||g' | tr ',' '\n' | sed 's|/tcp||g; s|/udp||g' | tr '\n' ' ') fi # Save metadata cat > "$app_dir/metadata.json" << METAEOF { "name": "$name", "source": "$source", "converted_at": "$(date -Iseconds)", "entrypoint": "$entrypoint", "cmd": "$cmd", "workdir": "$workdir", "user": "$user", "uid": "$uid", "gid": "$gid", "memory": "$memory", "network": "$network", "ports": "$ports", "exposed_ports": "$detected_ports", "lxc_path": "$lxc_path", "data_path": "/srv/$name" } METAEOF log_info "Conversion complete!" echo "" echo "Container: $name" echo " LXC Path: $lxc_path" echo " Data Path: /srv/$name" echo " Network: $network" [ -n "$detected_ports" ] && echo " Ports: $detected_ports" echo "" echo "To test: lxc-start -n $name -F" echo "To package: rezappctl package $name" } # ========================================== # Import Command (download image for offline use) # ========================================== cmd_import() { local image="$1" local tag="${2:-latest}" [ -z "$image" ] && { log_error "Image name required"; return 1; } local name="${image##*/}" name="${name%%:*}" local full_image="${image}:${tag}" local tarball="$CACHE_DIR/${name}.tar" log_info "Importing $full_image for offline use..." # Initialize container runtime (Docker or Podman) init_runtime || return 1 # Pull and export log_info "Pulling image via $CONTAINER_RUNTIME..." runtime_pull "$full_image" || { log_error "Failed to pull image"; return 1; } log_info "Exporting to cache..." mkdir -p "$CACHE_DIR" runtime_create --name rezapp-import-$$ "$full_image" >/dev/null 2>&1 runtime_export rezapp-import-$$ > "$tarball" runtime_rm rezapp-import-$$ >/dev/null 2>&1 # Save inspect data runtime_inspect "$full_image" > "$CACHE_DIR/${name}.inspect.json" log_info "Image cached: $tarball" echo "" echo "To convert offline: rezappctl convert --from-tar $tarball --name $name" } # ========================================== # Package Command # ========================================== cmd_package() { local name="$1" local install_after="" [ "$name" = "--install" ] && { install_after="1"; name="$2"; } [ -z "$name" ] && { log_error "App name required"; return 1; } local app_dir="$APPS_DIR/$name" local meta_file="$app_dir/metadata.json" local pkg_dir="$OUTPUT_DIR/secubox-app-$name" [ ! -f "$meta_file" ] && { log_error "App not found: $name (run convert first)"; return 1; } # Load metadata local source_image=$(jsonfilter -i "$meta_file" -e '@.source_image') local memory=$(jsonfilter -i "$meta_file" -e '@.memory') local uid=$(jsonfilter -i "$meta_file" -e '@.uid') local gid=$(jsonfilter -i "$meta_file" -e '@.gid') log_info "Generating package for: $name" # Create package structure rm -rf "$pkg_dir" mkdir -p "$pkg_dir/files/etc/config" mkdir -p "$pkg_dir/files/etc/init.d" mkdir -p "$pkg_dir/files/usr/sbin" # Generate Makefile cat > "$pkg_dir/Makefile" << MAKEEOF include \$(TOPDIR)/rules.mk PKG_NAME:=secubox-app-$name PKG_VERSION:=1.0.0 PKG_RELEASE:=1 include \$(INCLUDE_DIR)/package.mk define Package/secubox-app-$name SECTION:=secubox CATEGORY:=SecuBox SUBMENU:=Apps TITLE:=$name (via RezApp Forge) DEPENDS:=+lxc +lxc-common PKGARCH:=all endef define Package/secubox-app-$name/description $name - converted from Docker image $source_image Generated by RezApp Forge. endef define Package/secubox-app-$name/conffiles /etc/config/$name endef define Build/Compile endef define Package/secubox-app-$name/install \$(INSTALL_DIR) \$(1)/etc/config \$(INSTALL_CONF) ./files/etc/config/$name \$(1)/etc/config/ \$(INSTALL_DIR) \$(1)/etc/init.d \$(INSTALL_BIN) ./files/etc/init.d/$name \$(1)/etc/init.d/ \$(INSTALL_DIR) \$(1)/usr/sbin \$(INSTALL_BIN) ./files/usr/sbin/${name}ctl \$(1)/usr/sbin/ endef \$(eval \$(call BuildPackage,secubox-app-$name)) MAKEEOF # Generate UCI config cat > "$pkg_dir/files/etc/config/$name" << UCIEOF config main 'main' option enabled '0' option container '$name' option memory '$memory' option data_path '/srv/$name' UCIEOF # Generate init script cat > "$pkg_dir/files/etc/init.d/$name" << 'INITEOF' #!/bin/sh /etc/rc.common START=95 STOP=10 EXTRA_COMMANDS="status" EXTRA_HELP=" status Show container status" CONF="APPNAME" load_config() { . /lib/functions.sh config_load "$CONF" config_get ENABLED main enabled "0" config_get CONTAINER main container "APPNAME" } start() { load_config [ "$ENABLED" != "1" ] && { echo "APPNAME disabled"; return 0; } if lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "RUNNING"; then echo "APPNAME already running" else echo "Starting APPNAME..." lxc-start -n "$CONTAINER" -d sleep 3 lxc-info -n "$CONTAINER" | grep State fi } stop() { load_config if lxc-info -n "$CONTAINER" 2>/dev/null | grep -q "RUNNING"; then echo "Stopping APPNAME..." lxc-stop -n "$CONTAINER" else echo "APPNAME not running" fi } restart() { stop sleep 2 start } status() { load_config lxc-info -n "$CONTAINER" 2>/dev/null || echo "Container not found" } INITEOF sed -i "s/APPNAME/$name/g" "$pkg_dir/files/etc/init.d/$name" chmod +x "$pkg_dir/files/etc/init.d/$name" # Generate CLI tool cat > "$pkg_dir/files/usr/sbin/${name}ctl" << 'CTLEOF' #!/bin/sh # Generated by RezApp Forge CONF="APPNAME" CONTAINER="APPNAME" . /lib/functions.sh load_config() { config_load "$CONF" config_get CONTAINER main container "APPNAME" } usage() { cat << EOF Usage: APPNAMEctl Commands: start Start container stop Stop container restart Restart container status Show status logs Show logs shell Open shell enable Enable autostart disable Disable autostart EOF } case "$1" in start) /etc/init.d/APPNAME start ;; stop) /etc/init.d/APPNAME stop ;; restart) /etc/init.d/APPNAME restart ;; status) /etc/init.d/APPNAME status ;; logs) load_config; lxc-attach -n "$CONTAINER" -- tail -100 /var/log/*.log 2>/dev/null ;; shell) load_config; lxc-attach -n "$CONTAINER" -- /bin/sh ;; enable) uci set APPNAME.main.enabled=1; uci commit APPNAME; echo "Enabled" ;; disable) uci set APPNAME.main.enabled=0; uci commit APPNAME; echo "Disabled" ;; *) usage ;; esac CTLEOF sed -i "s/APPNAME/$name/g" "$pkg_dir/files/usr/sbin/${name}ctl" chmod +x "$pkg_dir/files/usr/sbin/${name}ctl" log_info "Package generated: $pkg_dir" echo "" echo "To build:" echo " 1. rsync -av $pkg_dir/ secubox-tools/local-feed/secubox-app-$name/" echo " 2. ./secubox-tools/local-build.sh build secubox-app-$name" } # ========================================== # Publish Command # ========================================== cmd_publish() { local name="$1" [ -z "$name" ] && { log_error "App name required"; return 1; } local app_dir="$APPS_DIR/$name" local meta_file="$app_dir/metadata.json" local catalog_dir="/usr/share/secubox/plugins/catalog" [ ! -f "$meta_file" ] && { log_error "App not found: $name"; return 1; } # Load metadata local source_image=$(jsonfilter -i "$meta_file" -e '@.source_image') log_info "Publishing $name to catalog..." mkdir -p "$catalog_dir" cat > "$catalog_dir/$name.json" << CATEOF { "id": "$name", "name": "$name", "category": "utilities", "runtime": "lxc", "maturity": "community", "description": "Converted from $source_image via RezApp Forge", "source": { "docker_image": "$source_image" }, "packages": ["secubox-app-$name"], "capabilities": ["lxc-container"], "requirements": { "arch": ["aarch64"], "min_ram_mb": 256, "min_storage_mb": 500 }, "actions": { "install": "${name}ctl enable", "status": "${name}ctl status" } } CATEOF log_info "Published to: $catalog_dir/$name.json" } # ========================================== # List Command # ========================================== cmd_list() { echo "Converted Apps:" echo "===============" if [ -d "$APPS_DIR" ]; then for app in "$APPS_DIR"/*/metadata.json; do [ -f "$app" ] || continue local name=$(dirname "$app") name="${name##*/}" local image=$(jsonfilter -i "$app" -e '@.source_image' 2>/dev/null) [ -z "$image" ] && image=$(jsonfilter -i "$app" -e '@.source' 2>/dev/null) printf " %-20s %s\n" "$name" "$image" done else echo " (none)" fi } # ========================================== # Cache Command (show cached images) # ========================================== cmd_cache() { echo "Cached Images (offline ready):" echo "===============================" if [ -d "$CACHE_DIR" ]; then local found=0 for tarball in "$CACHE_DIR"/*.tar; do [ -f "$tarball" ] || continue found=1 local name=$(basename "$tarball" .tar) local size=$(du -h "$tarball" | cut -f1) local inspect="$CACHE_DIR/${name}.inspect.json" local image="" if [ -f "$inspect" ]; then image=$(jsonfilter -i "$inspect" -e '@[0].RepoTags[0]' 2>/dev/null) fi printf " %-20s %8s %s\n" "$name" "$size" "${image:-local}" done [ "$found" = "0" ] && echo " (none)" else echo " (none)" fi echo "" echo "Convert offline: rezappctl convert --from-tar --name " } # ========================================== # Run Command (start/test container) # ========================================== cmd_run() { local name="$1" local foreground="" local shell="" shift while [ $# -gt 0 ]; do case "$1" in -f|--foreground) foreground="1"; shift ;; -s|--shell) shell="1"; shift ;; *) shift ;; esac done [ -z "$name" ] && { log_error "App name required"; return 1; } local lxc_path="$LXC_DIR/$name" [ ! -d "$lxc_path" ] && { log_error "Container not found: $name"; return 1; } # Check if already running if lxc-info -n "$name" 2>/dev/null | grep -q "RUNNING"; then if [ -n "$shell" ]; then log_info "Opening shell in running container..." lxc-attach -n "$name" -- /bin/sh return $? fi log_warn "Container already running" lxc-info -n "$name" return 0 fi if [ -n "$foreground" ]; then log_info "Starting $name in foreground (Ctrl+C to stop)..." lxc-start -n "$name" -F else log_info "Starting $name..." lxc-start -n "$name" -d sleep 3 lxc-info -n "$name" fi } cmd_stop() { local name="$1" [ -z "$name" ] && { log_error "App name required"; return 1; } if lxc-info -n "$name" 2>/dev/null | grep -q "RUNNING"; then log_info "Stopping $name..." lxc-stop -n "$name" else log_warn "Container not running" fi } # ========================================== # Expose Command (HAProxy integration) # ========================================== cmd_expose() { local name="$1" local domain="$2" local port="$3" [ -z "$name" ] && { log_error "App name required"; return 1; } local meta_file="$APPS_DIR/$name/metadata.json" [ ! -f "$meta_file" ] && { log_error "App not found: $name"; return 1; } # Auto-detect port from metadata if not specified if [ -z "$port" ]; then port=$(jsonfilter -i "$meta_file" -e '@.exposed_ports' 2>/dev/null | awk '{print $1}') fi [ -z "$port" ] && { log_error "Port required (or specify in metadata)"; return 1; } # Default domain [ -z "$domain" ] && domain="${name}.gk2.secubox.in" log_info "Exposing $name on $domain (port $port)..." # Check for haproxyctl if ! command -v haproxyctl >/dev/null 2>&1; then log_warn "haproxyctl not found - manual HAProxy config required" echo "" echo "Add to /srv/haproxy/config/haproxy.cfg:" echo " acl host_${name} hdr(host) -i $domain" echo " use_backend ${name}_backend if host_${name}" echo "" echo " backend ${name}_backend" echo " mode http" echo " server ${name} 192.168.255.1:$port check" return 0 fi # Add vhost via haproxyctl haproxyctl vhost add "$domain" # Add route to mitmproxy local routes_file="/srv/mitmproxy-in/haproxy-routes.json" if [ -f "$routes_file" ]; then log_info "Adding mitmproxy route..." # Use Python to safely add to JSON python3 << PYEOF import json with open('$routes_file', 'r') as f: routes = json.load(f) routes['$domain'] = ['192.168.255.1', $port] with open('$routes_file', 'w') as f: json.dump(routes, f, indent=2) print('Route added: $domain -> 192.168.255.1:$port') PYEOF fi log_info "Exposed: https://$domain -> 192.168.255.1:$port" echo "" echo "Restart mitmproxy to apply: /etc/init.d/mitmproxy restart" } # ========================================== # Help # ========================================== usage() { cat << EOF RezApp Forge - Docker/OCI to SecuBox LXC Converter Usage: rezappctl [options] Catalog Commands: catalog list List enabled catalogs catalog add [ns] Add catalog (optional namespace) catalog remove Remove catalog Search: search Search Docker Hub (no runtime needed) info Show image details (no runtime needed) Import (download for offline use): import [tag] Download and cache image for offline conversion Convert: convert [opts] Convert Docker/OCI image to LXC --name App name (default: from image) --tag Image tag (default: latest) --memory Memory limit (default: 512M) --network Network: host, bridge, none (default: host) Offline mode (no Docker/Podman needed): --from-tar Convert from local tarball --from-oci Convert from OCI image directory --offline Use cached tarball if available Run & Manage: run [-f] [-s] Start container (-f=foreground, -s=shell) stop Stop container list Show converted apps cache Show cached images Package & Publish: package Generate SecuBox package publish Add to app catalog Expose (HAProxy integration): expose [domain] [port] Expose via HAProxy/mitmproxy Runtime: Uses Docker, falls back to Podman if unavailable. For offline conversion, use --from-tar or --from-oci flags. Examples: # Online workflow rezappctl search heimdall rezappctl convert linuxserver/heimdall --name heimdall rezappctl run heimdall -f # Test in foreground rezappctl expose heimdall # Expose via HAProxy # Import for offline use rezappctl import linuxserver/heimdall latest rezappctl cache # Show cached images # Offline workflow (no Docker needed) rezappctl convert --from-tar /srv/rezapp/cache/heimdall.tar --name heimdall # Package for distribution rezappctl package heimdall rezappctl publish heimdall EOF } # ========================================== # Main # ========================================== load_config ensure_dirs case "$1" in catalog) case "$2" in list) cmd_catalog_list ;; add) shift 2; cmd_catalog_add "$@" ;; remove) shift 2; cmd_catalog_remove "$@" ;; *) usage ;; esac ;; search) shift; cmd_search "$@" ;; info) shift; cmd_info "$@" ;; import) shift; cmd_import "$@" ;; convert) shift; cmd_convert "$@" ;; run) shift; cmd_run "$@" ;; stop) shift; cmd_stop "$@" ;; package) shift; cmd_package "$@" ;; publish) shift; cmd_publish "$@" ;; expose) shift; cmd_expose "$@" ;; list) cmd_list ;; cache) cmd_cache ;; help|--help|-h) usage ;; *) usage ;; esac