#!/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; } # 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 # ========================================== cmd_convert() { local image="" local name="" local tag="latest" local memory="" local network="" local ports="" local mounts="" # 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 ;; -*) log_error "Unknown option: $1"; return 1 ;; *) image="$1"; shift ;; esac done [ -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" log_info "Converting $full_image -> $name" # Step 1: Ensure Docker is running if ! docker info >/dev/null 2>&1; then log_info "Starting Docker daemon..." /etc/init.d/dockerd start 2>/dev/null sleep 5 if ! docker info >/dev/null 2>&1; then log_error "Docker is not available" return 1 fi fi # Step 2: Pull image log_info "Pulling Docker image..." docker pull "$full_image" || { log_error "Failed to pull image"; return 1; } # Step 3: Inspect image log_info "Inspecting image metadata..." mkdir -p "$app_dir" docker 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) # Step 4: Export filesystem log_info "Exporting container filesystem..." docker create --name rezapp-export-$$ "$full_image" >/dev/null 2>&1 docker export rezapp-export-$$ > "$tarball" docker rm rezapp-export-$$ >/dev/null 2>&1 # Step 5: Create LXC rootfs log_info "Creating LXC container..." rm -rf "$lxc_path" mkdir -p "$lxc_path/rootfs" tar xf "$tarball" -C "$lxc_path/rootfs" # Step 6: Generate start script log_info "Generating start script..." local start_script="$lxc_path/rootfs/start-lxc.sh" cat > "$start_script" << STARTEOF #!/bin/sh # Auto-generated by RezApp Forge cd ${workdir:-/} # Run entrypoint/cmd exec ${entrypoint:-${cmd:-/bin/sh}} STARTEOF chmod +x "$start_script" # Step 7: Generate LXC config log_info "Generating LXC config..." # Parse user for UID/GID local uid="0" local gid="0" if [ -n "$user" ] && [ "$user" != "root" ]; then uid="${user%%:*}" gid="${user#*:}" [ "$gid" = "$user" ] && gid="$uid" fi # Convert memory to bytes local mem_bytes case "$memory" in *G) mem_bytes=$(( ${memory%G} * 1073741824 )) ;; *M) mem_bytes=$(( ${memory%M} * 1048576 )) ;; *) mem_bytes="$memory" ;; esac cat > "$lxc_path/config" << LXCEOF # LXC config for $name (auto-generated by RezApp Forge) lxc.uts.name = $name lxc.rootfs.path = dir:$lxc_path/rootfs lxc.net.0.type = none lxc.mount.auto = proc:mixed sys:ro lxc.mount.entry = /srv/$name config none bind,create=dir 0 0 lxc.cap.drop = sys_admin sys_module mac_admin mac_override lxc.cgroup2.memory.max = $mem_bytes lxc.init.uid = $uid lxc.init.gid = $gid lxc.init.cmd = /start-lxc.sh lxc.console.size = 1024 lxc.pty.max = 1024 lxc.cgroup2.devices.allow = c 1:* rwm lxc.cgroup2.devices.allow = c 5:* rwm lxc.cgroup2.devices.allow = c 136:* rwm lxc.seccomp.profile = lxc.start.auto = 0 LXCEOF # Step 8: Create data directory mkdir -p "/srv/$name" [ "$uid" != "0" ] && chown "$uid:$gid" "/srv/$name" # Step 9: Save metadata cat > "$app_dir/metadata.json" << METAEOF { "name": "$name", "source_image": "$full_image", "converted_at": "$(date -Iseconds)", "entrypoint": "$entrypoint", "cmd": "$cmd", "workdir": "$workdir", "user": "$user", "uid": "$uid", "gid": "$gid", "memory": "$memory", "network": "$network", "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 "" echo "To test: lxc-start -n $name -F" echo "To package: rezappctl package $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') printf " %-20s %s\n" "$name" "$image" done else echo " (none)" fi } # ========================================== # Help # ========================================== usage() { cat << EOF RezApp Forge - Docker to SecuBox 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 info Show image details Convert: convert [opts] Convert Docker image to LXC --name App name (default: from image) --tag Image tag (default: latest) --memory Memory limit (default: 512M) --network Network type (default: host) Package: package Generate SecuBox package publish Add to app catalog List: list Show converted apps Examples: rezappctl search heimdall rezappctl convert linuxserver/heimdall --name heimdall 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 "$@" ;; convert) shift; cmd_convert "$@" ;; package) shift; cmd_package "$@" ;; publish) shift; cmd_publish "$@" ;; list) cmd_list ;; help|--help|-h) usage ;; *) usage ;; esac