* * Actions: * create * modify * update * rebuild * ca-install * * modify fields: * port value: newExternalPort (z.B. "8466") * oder newExternalPort:internalPort (z.B. "8466:8465") * env value: VAR_NAME=VAR_VALUE (z.B. "TZ=Europe/Berlin") * volume value: hostPath:containerPath (z.B. "/mnt/user/data:/data") * network value: bridge|host|none * privileged value: true|false */ error_reporting(E_ALL); ini_set('display_errors', 1); // ── Unraid Setup (aus update_container) ────────────────────────────────── $docroot = '/usr/local/emhttp'; require_once "$docroot/webGui/include/Wrappers.php"; extract(parse_plugin_cfg('dynamix', true)); $_SERVER['REQUEST_URI'] = ''; $login_locale = _var($display, 'locale'); require_once "$docroot/plugins/dynamix.docker.manager/include/DockerClient.php"; $var = parse_ini_file('/var/local/emhttp/var.ini'); $DockerClient = new DockerClient(); $DockerUpdate = new DockerUpdate(); $DockerTemplates = new DockerTemplates(); $custom = DockerUtil::custom(); $subnet = DockerUtil::network($custom); $cpus = DockerUtil::cpus(); // ── Helper-Funktionen ──────────────────────────────────────────────────── function out(string $msg): void { echo $msg . "\n"; } function fail(string $msg): never { out("ERROR: " . $msg); exit(1); } function docker_exec(string $cmd): string { $proc = popen("/usr/bin/docker $cmd 2>&1", 'r'); $output = stream_get_contents($proc); pclose($proc); return trim($output); } function get_template_path(string $name): string { $dir = '/boot/config/plugins/dockerMan/templates-user'; $file = "$dir/my-$name.xml"; if (!file_exists($file)) { fail("Template not found: $file"); } return $file; } function rebuild_container(string $name, bool $pull_image = false, bool $force_start = false): void { global $DockerClient; $tmpl = get_template_path($name); $xml = file_get_contents($tmpl); [$cmd, $Name, $Repository] = xmlToCommand($tmpl); // Pull image if requested if ($pull_image) { out("Pulling image: $Repository"); $pull_out = docker_exec("pull $Repository"); if (strpos($pull_out, 'Error') !== false || strpos($pull_out, 'error') !== false) { fail("Image pull failed: $pull_out"); } out("Image pulled: $Repository"); } // Check if container is running $oldContainerInfo = $DockerClient->getContainerDetails($Name); $startContainer = $force_start; if (!empty($oldContainerInfo) && !empty($oldContainerInfo['State']) && !empty($oldContainerInfo['State']['Running'])) { $startContainer = true; out("Stopping container: $Name"); $DockerClient->stopContainer($Name); } // Convert create to run if we need to start if ($startContainer) { $cmd = str_replace('/docker create ', '/docker run -d ', $cmd); } // Remove old container out("Removing old container: $Name"); $DockerClient->removeContainer($Name); // Execute the docker command out("Creating container: $Name"); $proc = popen("$cmd 2>&1", 'r'); $output = stream_get_contents($proc); $rc = pclose($proc); if ($rc !== 0) { fail("Container creation failed (exit $rc): $output"); } out("Container created: $Name"); // Flush caches $DockerClient->flushCaches(); out("Done: $Name"); } /** * Set the text content of a DOM element. * This is the actual value Unraid uses (not the Default attribute). */ function set_config_value(DOMElement $config, string $value): void { // Remove existing text children while ($config->firstChild) { $config->removeChild($config->firstChild); } $config->appendChild(new DOMText($value)); // Also update Default attribute for consistency $config->setAttribute('Default', $value); } function modify_template(string $name, string $field, string $value): void { $tmpl = get_template_path($name); $xml = file_get_contents($tmpl); $dom = new DOMDocument(); $dom->loadXML($xml); switch ($field) { case 'port': // value: newExternalPort or newExternalPort:internalPort $parts = explode(':', $value, 2); $newExternal = $parts[0]; $newInternal = $parts[1] ?? null; $found = false; foreach ($dom->getElementsByTagName('Config') as $config) { if (strcasecmp($config->getAttribute('Type'), 'Port') === 0) { if ($newInternal !== null) { // Match by internal port (Target attribute) if ($config->getAttribute('Target') === $newInternal) { $config->setAttribute('Target', $newInternal); set_config_value($config, $newExternal); $found = true; break; } } else { // No internal port specified — change first Port config set_config_value($config, $newExternal); $found = true; break; } } } if (!$found) { fail("Port config not found in template" . ($newInternal !== null ? " (Target=$newInternal)" : "")); } break; case 'env': // value: VAR_NAME=VAR_VALUE $eqPos = strpos($value, '='); if ($eqPos === false) { fail("Env value must be VAR_NAME=VAR_VALUE, got: $value"); } $varName = substr($value, 0, $eqPos); $varValue = substr($value, $eqPos + 1); $found = false; foreach ($dom->getElementsByTagName('Config') as $config) { if (strcasecmp($config->getAttribute('Type'), 'Variable') === 0) { if ($config->getAttribute('Target') === $varName) { set_config_value($config, $varValue); $found = true; break; } } } if (!$found) { fail("Env var $varName not found in template"); } break; case 'volume': // value: hostPath:containerPath $colonPos = strpos($value, ':'); if ($colonPos === false) { fail("Volume value must be hostPath:containerPath, got: $value"); } $hostPath = substr($value, 0, $colonPos); $containerPath = substr($value, $colonPos + 1); $found = false; foreach ($dom->getElementsByTagName('Config') as $config) { if (strcasecmp($config->getAttribute('Type'), 'Path') === 0) { if ($config->getAttribute('Target') === $containerPath) { set_config_value($config, $hostPath); $found = true; break; } } } if (!$found) { fail("Volume target $containerPath not found in template"); } break; case 'network': $networks = $dom->getElementsByTagName('Network'); if ($networks->length === 0) { fail("No Network element in template"); } $networks->item(0)->nodeValue = $value; break; case 'privileged': $privs = $dom->getElementsByTagName('Privileged'); if ($privs->length === 0) { fail("No Privileged element in template"); } $privs->item(0)->nodeValue = $value; break; default: fail("Unknown field: $field (use: port|env|volume|network|privileged)"); } // Write modified template $dom->save($tmpl); out("Template modified: $name ($field = $value)"); // Rebuild container (force start so it's running after modify) rebuild_container($name, false, true); } function ca_install(string $template_url, string $name, string $overrides_json, bool $start): void { if (!preg_match('/^[a-zA-Z0-9._-]{1,128}$/', $name)) { fail('Invalid container name'); } $url = parse_url($template_url); if (!is_array($url) || ($url['scheme'] ?? '') !== 'https' || empty($url['host'])) { fail('Community Applications template must use HTTPS'); } $host = strtolower((string)$url['host']); if ($host === 'localhost' || str_ends_with($host, '.local')) { fail('Private template hosts are rejected'); } if (filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { fail('Private or reserved template IPs are rejected'); } $template_path = "/boot/config/plugins/dockerMan/templates-user/my-$name.xml"; if (file_exists($template_path)) fail("Template already exists: $name"); $existing = trim(docker_exec( 'ps -a --filter ' . escapeshellarg("name=^/$name$") . ' --format ' . escapeshellarg('{{.Names}}') )); if (in_array($name, preg_split('/\R/', $existing) ?: [], true)) fail("Container already exists: $name"); $context = stream_context_create(['http' => [ 'timeout' => 30, 'follow_location' => 0, 'user_agent' => 'MUA Community Applications Installer', ]]); $xml = @file_get_contents($template_url, false, $context, 0, 1024 * 1024 + 1); if ($xml === false || strlen($xml) === 0) fail('Unable to download Community Applications template'); if (strlen($xml) > 1024 * 1024) fail('Community Applications template is too large'); $dom = new DOMDocument(); $old = libxml_use_internal_errors(true); $loaded = $dom->loadXML($xml, LIBXML_NONET | LIBXML_NOBLANKS); libxml_clear_errors(); libxml_use_internal_errors($old); if (!$loaded || !$dom->documentElement || $dom->documentElement->nodeName !== 'Container') { fail('Invalid Community Applications container template'); } $repositories = $dom->getElementsByTagName('Repository'); if ($repositories->length !== 1 || trim($repositories->item(0)->textContent) === '') { fail('Template has no valid Docker repository'); } $names = $dom->getElementsByTagName('Name'); if ($names->length === 0) { $name_node = $dom->createElement('Name', $name); $dom->documentElement->insertBefore($name_node, $dom->documentElement->firstChild); } else { $names->item(0)->nodeValue = $name; } $overrides = json_decode($overrides_json, true); if (!is_array($overrides) || count($overrides) > 32) fail('Invalid overrides object'); foreach ($overrides as $target => $value) { if (!is_string($target) || !is_string($value) || strlen($value) > 4096) fail('Invalid override'); $found = false; foreach ($dom->getElementsByTagName('Config') as $config) { if ($config->getAttribute('Target') === $target) { set_config_value($config, $value); $found = true; break; } } if (!$found) fail("Override target not present in template: $target"); } $dom->formatOutput = true; if ($dom->save($template_path) === false) fail('Unable to write Unraid user template'); chmod($template_path, 0600); out("Community Applications template saved: $name"); rebuild_container($name, true, $start); out('Container is managed by the Unraid user template'); } // ── Main ───────────────────────────────────────────────────────────────── $argv = $_SERVER['argv']; $action = $argv[1] ?? ''; $arg1 = $argv[2] ?? ''; $arg2 = $argv[3] ?? ''; $arg3 = $argv[4] ?? ''; switch ($action) { case 'create': if (!$arg1) fail("Missing template name"); out("Creating container from template: $arg1"); rebuild_container($arg1, true); break; case 'modify': if (!$arg1 || !$arg2 || !$arg3) fail("Usage: modify "); modify_template($arg1, $arg2, $arg3); break; case 'update': if (!$arg1) fail("Missing container name"); out("Updating container: $arg1"); rebuild_container($arg1, true); break; case 'rebuild': if (!$arg1) fail("Missing container name"); out("Rebuilding container: $arg1"); rebuild_container($arg1, false); break; case 'ca-install': if (!$arg1 || !$arg2) fail('Usage: ca-install '); ca_install($arg1, $arg2, $arg3 ?: '{}', ($argv[5] ?? 'false') === 'true'); break; default: fail("Unknown action: $action. Usage: create|modify|update|rebuild|ca-install"); }