secubox-openwrt/package/secubox/secubox-app-rezapp/files/usr/sbin/rezappctl
CyberMind-FR 66b58c74d6 feat(catalog): Add Streamlit Forge and RezApp Forge to KISS Apps
- luci-app-streamlit-forge: Streamlit app publishing platform
  - Category: productivity, runtime: lxc
  - Templates, SSL exposure, mesh publishing

- luci-app-rezapp: Docker to LXC app converter
  - Category: system, runtime: native
  - Catalog browsing, package generation

- Updated new_releases section
- Total plugins: 37 → 39

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-11 09:44:08 +01:00

670 lines
16 KiB
Bash

#!/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 <app-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 <command>
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 <command> [options]
Catalog Commands:
catalog list List enabled catalogs
catalog add <name> [ns] Add catalog (optional namespace)
catalog remove <name> Remove catalog
Search:
search <query> Search Docker Hub
info <image> Show image details
Convert:
convert <image> [opts] Convert Docker image to LXC
--name <name> App name (default: from image)
--tag <tag> Image tag (default: latest)
--memory <limit> Memory limit (default: 512M)
--network <type> Network type (default: host)
Package:
package <name> Generate SecuBox package
publish <name> 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