secubox-openwrt/package/secubox/secubox-app-rezapp/files/usr/sbin/rezappctl
CyberMind-FR 9081444c7a feat(streamlit-control): Phase 3 - auto-refresh, permissions, UI improvements
Streamlit Control Dashboard Phase 3:
- Add auto-refresh toggle to all main pages (10s/30s/60s intervals)
- Add permission-aware UI with can_write() and is_admin() helpers
- Containers page: tabs (All/Running/Stopped), search filter, info panels
- Security page: better CrowdSec parsing, threat table, raw data viewer
- Streamlit apps page: restart button, delete confirmation dialog
- Network page: HAProxy filter, WireGuard/DNS placeholders

fix(crowdsec-dashboard): Handle RPC error codes in overview.js

Fix TypeError when CrowdSec RPC returns error code instead of object.
Added type check to treat non-objects as empty {} in render/pollData.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-11 14:54:30 +01:00

1247 lines
32 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; }
# 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 <app-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 <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' 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 <file> --name <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 <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 (no runtime needed)
info <image> Show image details (no runtime needed)
Import (download for offline use):
import <image> [tag] Download and cache image for offline conversion
Convert:
convert <image> [opts] Convert Docker/OCI 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: host, bridge, none (default: host)
Offline mode (no Docker/Podman needed):
--from-tar <file> Convert from local tarball
--from-oci <dir> Convert from OCI image directory
--offline Use cached tarball if available
Run & Manage:
run <name> [-f] [-s] Start container (-f=foreground, -s=shell)
stop <name> Stop container
list Show converted apps
cache Show cached images
Package & Publish:
package <name> Generate SecuBox package
publish <name> Add to app catalog
Expose (HAProxy integration):
expose <name> [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