diff --git a/plugins/dynamix.docker.manager/DockerContainers.page b/plugins/dynamix.docker.manager/DockerContainers.page index 604f7970c..9d8712a2e 100644 --- a/plugins/dynamix.docker.manager/DockerContainers.page +++ b/plugins/dynamix.docker.manager/DockerContainers.page @@ -84,12 +84,12 @@ img.stopped{opacity:0.3;} $updateStatus = ($updateStatus == "true" or $updateStatus == "undef" ) ? 'true' : 'false'; $running = ($ct['Running']) ? 'true' : 'false'; $webGuiUrl = $info[$name]['url']; - $contextMenus[] = sprintf("addDockerContainerContext('%s', '%s', '%s', %s, %s, %s, '%s');", addslashes($ct['Name']), addslashes($ct['ImageId']), addslashes($info[$name]['template']), $running, $updateStatus, $is_autostart, addslashes($webGuiUrl)); + $contextMenus[] = sprintf("addDockerContainerContext('%s', '%s', '%s', %s, %s, %s, '%s', '%s');", addslashes($ct['Name']), addslashes($ct['ImageId']), addslashes($info[$name]['template']), $running, $updateStatus, $is_autostart, addslashes($webGuiUrl), $ct["Id"]); $shape = ($ct["Running"]) ? "play" : "square"; $status = ($ct["Running"]) ? "started" : "stopped"; $Icon = $info[$name]['icon']; - if ( $Icon == "#" ){ + if ( ! $Icon ){ $Icon = "/plugins/dynamix.docker.manager/images/question.png"; } @@ -144,6 +144,7 @@ img.stopped{opacity:0.3;} echo htmlspecialchars($ct['Image']); } ?> + ${ct[BaseImage]}";?> - ", $ports); ?> - ", $paths); ?> + ", $ports); ?> + ", $paths); ?> > -
Created
+
Created
@@ -243,5 +244,7 @@ $(function() { context.init({ preventDoubleContext: false }); + + $('.docker_readmore').readmore({maxHeight:48, moreLink: '', lessLink: ''}); }); diff --git a/plugins/dynamix.docker.manager/DockerSettings.page b/plugins/dynamix.docker.manager/DockerSettings.page index 2ba7bbbbe..097dbe432 100644 --- a/plugins/dynamix.docker.manager/DockerSettings.page +++ b/plugins/dynamix.docker.manager/DockerSettings.page @@ -1,6 +1,7 @@ Menu="OtherSettings" Title="Docker" Icon="dynamix.docker.manager.png" +Markdown="false" --- Missing docker.cfg file!

"; - return; -} -$dockercfg = parse_ini_file($cfgfile); -if (!array_key_exists('DOCKER_ENABLED', $dockercfg)) { - $dockercfg['DOCKER_ENABLED'] = 'no'; -} - // Check for nodatacow flag on Docker file; display warning $realfile = $dockercfg['DOCKER_IMAGE_FILE']; if (file_exists($realfile)) { @@ -54,112 +43,158 @@ if (file_exists($realfile)) { } ?> + + +
+ + + +
+
Enable Docker:
+
+ +
+
+
+

Before you can start the Docker service for the first time, please specify an image + file for Docker to install to. Once started, Docker will always automatically start + after the array has been started.

+
+ - - - -Enable Docker: -: +
+
Docker vdisk size:
+
GB
+
+
+

If the system needs to create a new docker image file, this is the default size to use specified in GB.

+

To resize an existing image file, specify the new size here. Next time the Docker service is started the file (and file system) will increased to the new size (but never decreased).

+
-> Before you can start the Docker service for the first time, please specify an image -> file for Docker to install to. Once started, Docker will always automatically start -> after the array has been started. +
+
+
Docker storage location:
+
+
+
+

You must specify an image file for Docker. The system will automatically create this file when the Docker service is first started.

+
+
-Default image size: -: GB + -> If the system needs to create a new docker image file, this is the default size to use -> specified in GB. -> -> To resize an existing image file, specify the new size here. Next time the Docker service is -> started the file (and file system) will increased to the new size (but never decreased). +
+
+
Docker version:
+
getInfo(); echo $arrInfo['Version']; ?>
+
+
+

This is the docker version.

+
-Docker image: -: +
+
Docker storage location:
+
+
+
+

This is the docker volume.

+
+
-> You must specify an image file for Docker. The system will automatically -> create this file when the Docker service is first started. If you do not want Docker -> to run at all, set this field blank and click **Start**. - -  -: - -
- -
- - - -Enable Docker: -: - -> Stopping the Docker service will first stop all the running containers. - -Docker version: -: getInfo(); echo $arrInfo['Version']; ?> - -> This is the docker version. - -Docker image: -: - -> This is the docker volume. - -  -: - -
-
Docker volume info
- -btrfs filesystem show: -: ".shell_exec("btrfs filesystem show /var/lib/docker")."";?> - -
- - -btrfs scrub status: -: " . implode("\n", $scrub_status) . "";?> - - - - - - -  -: - -> **Scrub** runs the *btrfs scrub* program to check file system integrity. -> -> If repair is needed you should uncheck the *Don't fix file system errors* option and -> run a second Scrub pass; this will permit *btrfs scrub* to fix the file system. - - - - - - -  -: *Running* - -> **Cancel** will cancel the Scrub operation in progress. - -
+ +
+
+
Default appdata storage location:
+
+
+
+

You can specify a folder to automatically generate and store subfolders containing configuration files for each Docker app (via the /config mapped volume).

+

Only used when adding new Docker apps. Editing existing Docker apps will not be affected by this setting.

+
+ +
+
Auto-map user shares to containers as /unraid:
+
+ +
+
+
+

You can expose all of your user shares (/mnt/user) to a folder named /unraid within Docker containers.

+

Only used when adding new Docker apps. Editing existing Docker apps will not be affected by this setting.

+
+
+ +
+
 
+
+
+ + + +
+
Docker volume info
+ +
+
btrfs filesystem show:
+
".shell_exec("btrfs filesystem show /var/lib/docker").""?>
+
+ +
+ + +
+
btrfs scrub status:
+
".implode("\n", $scrub_status).""?>
+
+ + + + + + +
+
 
+
+
+
+

Scrub runs the btrfs scrub program to check file system integrity.

+

If repair is needed you should uncheck the Don't fix file system errors option and run a second Scrub pass; this will permit btrfs scrub to fix the file system.

+
+ + + + + + +
+
 
+
Running
+
+
+

Cancel will cancel the Scrub operation in progress.

+
+ +
+ +
+ diff --git a/plugins/dynamix.docker.manager/include/CreateDocker.php b/plugins/dynamix.docker.manager/include/CreateDocker.php index b697ea724..ce30dbbe8 100644 --- a/plugins/dynamix.docker.manager/include/CreateDocker.php +++ b/plugins/dynamix.docker.manager/include/CreateDocker.php @@ -17,6 +17,15 @@ $DockerClient = new DockerClient(); $DockerUpdate = new DockerUpdate(); $DockerTemplates = new DockerTemplates(); +# ███████╗██╗ ██╗███╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗███████╗ +# ██╔════╝██║ ██║████╗ ██║██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝ +# █████╗ ██║ ██║██╔██╗ ██║██║ ██║ ██║██║ ██║██╔██╗ ██║███████╗ +# ██╔══╝ ██║ ██║██║╚██╗██║██║ ██║ ██║██║ ██║██║╚██╗██║╚════██║ +# ██║ ╚██████╔╝██║ ╚████║╚██████╗ ██║ ██║╚██████╔╝██║ ╚████║███████║ +# ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ + +$echo = function($m){echo "
".print_r($m,true)."
";}; + function prepareDir($dir){ if (strlen($dir)){ if ( ! is_dir($dir) && ! is_file($dir)){ @@ -30,7 +39,6 @@ function prepareDir($dir){ function ContainerExist($container){ global $DockerClient; - $all_containers = $DockerClient->getDockerContainers(); if ( ! $all_containers) { return FALSE; } foreach ($all_containers as $ct) { @@ -42,60 +50,67 @@ function ContainerExist($container){ return False; } +function ImageExist($image){ + global $DockerClient; + $all_images = $DockerClient->getDockerImages(); + if ( ! $all_images) { return FALSE; } + foreach ($all_images as $img) { + if ( ! is_bool(strpos($img['Tags'][0], $image)) ){ + return True; + break; + } + } + return False; +} + function trimLine($text){ return preg_replace("/([\n^])[\s]+/", '$1', $text); } +$pullecho = function($line) { + global $alltotals; + $cnt = json_decode( $line, TRUE ); + $id = ( isset( $cnt['id'] )) ? $cnt['id'] : ""; + $status = ( isset( $cnt['status'] )) ? $cnt['status'] : ""; + if (strlen(trim($status)) && strlen(trim($id))) { + if ( isset($cnt['progressDetail']['total']) && $cnt['progressDetail']['total'] > 0 ) { + $alltotals[$cnt['id']] = $cnt['progressDetail']['total']; + } + echo "\n"; + @flush(); + } + if ($status == "Downloading") { + $total = $cnt['progressDetail']['total']; + $current = $cnt['progressDetail']['current']; + $alltotals[$cnt['id']] = $cnt['progressDetail']['current']; + if ($total > 0) { + $percentage = round(($current/$total) * 100); + echo "\n"; + } else { + // Docker must not know the total download size (http-chunked or something?) + // just show the current download progress without the percentage + echo "\n"; + } + @flush(); + } +}; + + function pullImage($image) { + global $DockerClient, $pullecho, $alltotals; + $alltotals = array(); if (! preg_match("/:[\w]*$/i", $image)) $image .= ":latest"; readfile("/usr/local/emhttp/plugins/dynamix.docker.manager/log.htm"); - echo ''; - echo ""; + echo ""; @flush(); - $fp = stream_socket_client('unix:///var/run/docker.sock', $errno, $errstr); - if ($fp === false) { - echo "Couldn't create socket: [$errno] $errstr"; - return NULL; - } - $out="POST /images/create?fromImage=$image HTTP/1.1\r\nConnection: Close\r\n\r\n"; - fwrite($fp, $out); - $cid = ""; - $cstatus=""; - $alltotals = []; - while (!feof($fp)) { - $cnt = json_decode( fgets($fp, 5000), TRUE ); - $id = ( isset( $cnt['id'] )) ? $cnt['id'] : ""; - if ($id != $cid && strlen($id)) { - $cid = $id; - $cstatus = ""; - echo ""; - @flush(); - } - $status = ( isset( $cnt['status'] )) ? $cnt['status'] : ""; - if ($status != $cstatus && strlen($status)) { - if ( isset($cnt['progressDetail']['total']) && $cnt['progressDetail']['total'] > 0 ) { - $alltotals[$cnt['id']] = $cnt['progressDetail']['total']; - } - $cstatus = $status; - echo ""; - @flush(); - } - if ($status == "Downloading") { - $total = $cnt['progressDetail']['total']; - $current = $cnt['progressDetail']['current']; - $alltotals[$cnt['id']] = $cnt['progressDetail']['current']; - if ($total > 0) { - $percentage = round(($current/$total) * 100); - echo "\n"; - } else { - // Docker must not know the total download size (http-chunked or something?) - // just show the current download progress without the percentage - echo "\n"; - } - @flush(); - } - } + $DockerClient->pullImage($image, $pullecho); echo "\n"; } @@ -109,175 +124,291 @@ function sizeToHuman($size) { return ceil($size) . " " . $units[$unitsIndex]; } -function xmlToCommand($xmlFile){ - global $var; - $doc = new DOMDocument(); - $doc->loadXML($xmlFile); - - $Name = $doc->getElementsByTagName( "Name" )->item(0)->nodeValue; - $cmdName = (strlen($Name)) ? '--name="' . $Name . '"' : ""; - $Privileged = $doc->getElementsByTagName( "Privileged" )->item(0)->nodeValue; - $cmdPrivileged = (strtolower($Privileged) == 'true') ? '--privileged="true"' : ""; - $Repository = $doc->getElementsByTagName( "Repository" )->item(0)->nodeValue; - $Mode = $doc->getElementsByTagName( "Mode" )->item(0)->nodeValue; - $cmdMode = '--net="'.strtolower($Mode).'"'; - $BindTime = $doc->getElementsByTagName( "BindTime" )->item(0)->nodeValue; - // $cmdBindTime = (strtolower($BindTime) == "true") ? '"/etc/localtime":"/etc/localtime":ro' : ''; - $cmdBindTime = (strtolower($BindTime) == "true") ? 'TZ="' . $var['timeZone'] . '"' : ''; - - $Ports = array(''); - foreach($doc->getElementsByTagName('Port') as $port){ - $ContainerPort = $port->getElementsByTagName( "ContainerPort" )->item(0)->nodeValue; - if (! strlen($ContainerPort)){ continue; } - $HostPort = $port->getElementsByTagName( "HostPort" )->item(0)->nodeValue; - $Protocol = $port->getElementsByTagName( "Protocol" )->item(0)->nodeValue; - $Ports[] = sprintf("%s:%s/%s", $HostPort, $ContainerPort, $Protocol); - } - - $Volumes = array(''); - foreach($doc->getElementsByTagName('Volume') as $volume){ - $ContainerDir = $volume->getElementsByTagName( "ContainerDir" )->item(0)->nodeValue; - if (! strlen($ContainerDir)){ continue; } - $HostDir = $volume->getElementsByTagName( "HostDir" )->item(0)->nodeValue; - $DirMode = $volume->getElementsByTagName( "Mode" )->item(0)->nodeValue; - $Volumes[] = sprintf( '"%s":"%s":%s', $HostDir, $ContainerDir, $DirMode); - } - - // if (strlen($cmdBindTime)) { - // $Volumes[] = $cmdBindTime; - // } - - $Variables = array(''); - foreach($doc->getElementsByTagName('Variable') as $variable){ - $VariableName = $variable->getElementsByTagName( "Name" )->item(0)->nodeValue; - if (! strlen($VariableName)){ continue; } - $VariableValue = $variable->getElementsByTagName( "Value" )->item(0)->nodeValue; - $Variables[] = sprintf('%s="%s"', $VariableName, $VariableValue); - } - - if (strlen($cmdBindTime)) { - $Variables[] = $cmdBindTime; - } - - $templateExtraParams = ''; - if ( $doc->getElementsByTagName( "ExtraParams" )->length > 0 ) { - $templateExtraParams = $doc->getElementsByTagName( "ExtraParams" )->item(0)->nodeValue; - } - - $cmd = sprintf('/plugins/dynamix.docker.manager/scripts/docker run -d %s %s %s %s %s %s %s %s', $cmdName, $cmdMode, $cmdPrivileged, implode(' -e ', $Variables), - implode(' -p ', $Ports), implode(' -v ', $Volumes), $templateExtraParams, $Repository); - $cmd = preg_replace('/\s+/', ' ', $cmd); - - return array($cmd, $Name, $Repository); +function xml_encode($string) { + return htmlspecialchars($string, ENT_XML1, 'UTF-8'); } - -function addElement($doc, $el, $elName, $elVal){ - $node = $el->appendChild($doc->createElement($elName)); - $node->appendChild($doc->createTextNode($elVal)); - return $node; +function xml_decode($string) { + return strval(html_entity_decode($string, ENT_XML1, 'UTF-8')); } function postToXML($post, $setOwnership = FALSE){ - global $DockerUpdate; - $doc = new DOMDocument('1.0', 'utf-8'); - $doc->preserveWhiteSpace = false; - $doc->formatOutput = true; - $root = $doc->createElement('Container'); - $root = $doc->appendChild($root); + $dom = new domDocument; + $dom->appendChild($dom->createElement( "Container" )); + $xml = simplexml_import_dom( $dom ); + $xml["version"] = 2; + $xml->Name = xml_encode($post['contName']); + $xml->Repository = xml_encode($post['contRepository']); + $xml->Registry = xml_encode($post['contRegistry']); + $xml->Network = xml_encode($post['contNetwork']); + $xml->Privileged = (strtolower($post["contPrivileged"]) == 'on') ? 'true' : 'false'; + $xml->Support = xml_encode($post['contSupport']); + $xml->Overview = xml_encode($post['contOverview']); + $xml->Category = xml_encode($post['contCategory']); + $xml->WebUI = xml_encode($post['contWebUI']); + $xml->Icon = xml_encode($post['contIcon']); + $xml->ExtraParams = xml_encode($post['contExtraParams']); - $docName = $root->appendChild($doc->createElement('Name')); - if ( isset( $post[ 'Description' ] )) addElement($doc, $root, 'Description', $post[ 'Description' ]); - if ( isset( $post[ 'Registry' ] )) addElement($doc, $root, 'Registry', $post[ 'Registry' ]); - $docRepository = $root->appendChild($doc->createElement('Repository')); - $BindTime = $root->appendChild($doc->createElement('BindTime')); - $Privileged = $root->appendChild($doc->createElement('Privileged')); - $Environment = $root->appendChild($doc->createElement('Environment')); - $docNetworking = $root->appendChild($doc->createElement('Networking')); - $Data = $root->appendChild($doc->createElement('Data')); - $Version = $root->appendChild($doc->createElement('Version')); - $Mode = $docNetworking->appendChild($doc->createElement('Mode')); - $Publish = $docNetworking->appendChild($doc->createElement('Publish')); - $Name = preg_replace('/\s+/', '', $post["containerName"]); + # V1 compatibility + $xml->Description = xml_encode($post['contOverview']); + $xml->Networking->Mode = xml_encode($post['contNetwork']); + $xml->Networking->addChild("Publish"); + $xml->addChild("Data"); + $xml->addChild("Environment"); - // Editor Values - if ( isset( $post[ 'WebUI' ] )) addElement($doc, $root, 'WebUI', $post[ 'WebUI' ]); - if ( isset( $post[ 'Banner' ] )) addElement($doc, $root, 'Banner', $post[ 'Banner' ]); - if ( isset( $post[ 'Icon' ] )) addElement($doc, $root, 'Icon', $post[ 'Icon' ]); - if ( isset( $post[ 'ExtraParams' ] )) addElement($doc, $root, 'ExtraParams', $post[ 'ExtraParams' ]); - - $docName->appendChild($doc->createTextNode($Name)); - $docRepository->appendChild($doc->createTextNode($post["Repository"])); - $BindTime->appendChild($doc->createTextNode((strtolower($post["BindTime"]) == 'on') ? 'true' : 'false')); - $Privileged->appendChild($doc->createTextNode((strtolower($post["Privileged"]) == 'on') ? 'true' : 'false')); - $Mode->appendChild($doc->createTextNode(strtolower($post["NetworkType"]))); - - for ($i = 0; $i < count($post["hostPort"]); $i++){ - if (! strlen($post["containerPort"][$i])) { continue; } - $protocol = $post["portProtocol"][$i]; - $Port = $Publish->appendChild($doc->createElement('Port')); - $HostPort = $Port->appendChild($doc->createElement('HostPort')); - $ContainerPort = $Port->appendChild($doc->createElement('ContainerPort')); - $Protocol = $Port->appendChild($doc->createElement('Protocol')); - $HostPort->appendChild($doc->createTextNode(trim($post["hostPort"][$i]))); - $ContainerPort->appendChild($doc->createTextNode($post["containerPort"][$i])); - $Protocol->appendChild($doc->createTextNode($protocol)); - } - - for ($i = 0; $i < count($post["VariableName"]); $i++){ - if (! strlen($post["VariableName"][$i])) { continue; } - $Variable = $Environment->appendChild($doc->createElement('Variable')); - $VariableName = $Variable->appendChild($doc->createElement('Name')); - $VariableValue = $Variable->appendChild($doc->createElement('Value')); - $VariableName->appendChild($doc->createTextNode(trim($post["VariableName"][$i]))); - $VariableValue->appendChild($doc->createTextNode(trim($post["VariableValue"][$i]))); - } - - for ($i = 0; $i < count($post["hostPath"]); $i++){ - if (! strlen($post["hostPath"][$i])) { continue; } - if (! strlen($post["containerPath"][$i])) { continue; } - $tmpMode = $post["hostWritable"][$i]; - if ($setOwnership){ - prepareDir($post["hostPath"][$i]); + for ($i = 0; $i < count($post["confName"]); $i++) { + $Type = $post['confType'][$i]; + $config = $xml->addChild('Config'); + $config->{0} = xml_encode($post['confValue'][$i]); + $config['Name'] = xml_encode($post['confName'][$i]); + $config['Target'] = xml_encode($post['confTarget'][$i]); + $config['Default'] = xml_encode($post['confDefault'][$i]); + $config['Mode'] = xml_encode($post['confMode'][$i]); + $config['Description'] = xml_encode($post['confDescription'][$i]); + $config['Type'] = xml_encode($post['confType'][$i]); + $config['Display'] = xml_encode($post['confDisplay'][$i]); + $config['Required'] = xml_encode($post['confRequired'][$i]); + $config['Mask'] = xml_encode($post['confMask'][$i]); + # V1 compatibility + if ($Type == "Port"){ + $port = $xml->Networking->Publish->addChild("Port"); + $port->HostPort = $post['confValue'][$i]; + $port->ContainerPort = $post['confTarget'][$i]; + $port->Protocol = $post['confMode'][$i]; + } else if ($Type == "Path"){ + $path = $xml->Data->addChild("Volume"); + $path->HostDir = $post['confValue'][$i]; + $path->ContainerDir = $post['confTarget'][$i]; + $path->Mode = $post['confMode'][$i]; + } else if ($Type == "Variable"){ + $variable = $xml->Environment->addChild("Variable"); + $variable->Value = $post['confValue'][$i]; + $variable->Name = $post['confTarget'][$i]; + $variable->Mode = $post['confMode'][$i]; } - $Volume = $Data->appendChild($doc->createElement('Volume')); - $HostDir = $Volume->appendChild($doc->createElement('HostDir')); - $ContainerDir = $Volume->appendChild($doc->createElement('ContainerDir')); - $DirMode = $Volume->appendChild($doc->createElement('Mode')); - $HostDir->appendChild($doc->createTextNode($post["hostPath"][$i])); - $ContainerDir->appendChild($doc->createTextNode($post["containerPath"][$i])); - $DirMode->appendChild($doc->createTextNode($tmpMode)); } - - $currentVersion = $DockerUpdate->getRemoteVersion($post["Registry"], $post["Repository"]); - $Version->appendChild($doc->createTextNode($currentVersion)); - - return $doc->saveXML(); + $dom = new DOMDocument('1.0'); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + return $dom->saveXML(); } -if ($_POST){ +function xmlToVar($xml) { + global $var; + $out = array(); + $xml = (is_file($xml)) ? simplexml_load_file($xml) : simplexml_load_string($xml); + + $out['Name'] = xml_decode($xml->Name); + $out['Repository'] = xml_decode($xml->Repository); + $out['Registry'] = xml_decode($xml->Registry); + $out['Network'] = (isset($xml->Network)) ? xml_decode($xml->Network) : xml_decode($xml->Network['Default']); + $out['Privileged'] = xml_decode($xml->Privileged); + $out['Support'] = xml_decode($xml->Support); + $out['Overview'] = stripslashes(xml_decode($xml->Overview)); + $out['Category'] = xml_decode($xml->Category); + $out['WebUI'] = xml_decode($xml->WebUI); + $out['Icon'] = xml_decode($xml->Icon); + $out['ExtraParams'] = xml_decode($xml->ExtraParams); + + $out['Config'] = array(); + if (isset($xml->Config)) { + foreach ($xml->Config as $config) { + $c = array(); + $c['Value'] = strlen(xml_decode($config)) ? xml_decode($config) : xml_decode($config['Default']) ; + foreach ($config->attributes() as $key => $value) $c[$key] = xml_decode(xml_decode($value)); + $out['Config'][] = $c; + } + } + + # V1 compatibility + if ($xml["version"] != "2"){ + if (isset($xml->Networking->Mode)){ + $out['Network'] = xml_decode($xml->Networking->Mode); + } + if (isset($xml->Description)) { + $out['Overview'] = stripslashes(xml_decode($xml->Description)); + } + + if (isset($xml->Networking->Publish->Port)) { + $portNum = 0; + foreach ($xml->Networking->Publish->Port as $port) { + $portNum += 1; + $out['Config'][] = array('Name' => "Port ${portNum}", + 'Target' => xml_decode($port->ContainerPort), + 'Default' => xml_decode($port->HostPort), + 'Value' => xml_decode($port->HostPort), + 'Mode' => xml_decode($port->Protocol), + 'Description' => '', + 'Type' => 'Port', + 'Display' => 'always', + 'Required' => 'true', + 'Mask' => 'false', + ); + } + } + + if (isset($xml->Data->Volume)) { + $volNum = 0; + foreach ($xml->Data->Volume as $vol) { + $volNum += 1; + $out['Config'][] = array('Name' => "Path ${volNum}", + 'Target' => xml_decode($vol->ContainerDir), + 'Default' => xml_decode($vol->HostDir), + 'Value' => xml_decode($vol->HostDir), + 'Mode' => xml_decode($vol->Mode), + 'Description' => '', + 'Type' => 'Path', + 'Display' => 'always', + 'Required' => 'true', + 'Mask' => 'false', + ); + } + } + + if (isset($xml->Environment->Variable)) { + $varNum = 0; + foreach ($xml->Environment->Variable as $var) { + $varNum += 1; + $out['Config'][] = array('Name' => "Variable ${varNum}", + 'Target' => xml_decode($var->Name), + 'Default' => xml_decode($var->Value), + 'Value' => xml_decode($var->Value), + 'Mode' => '', + 'Description' => '', + 'Type' => 'Variable', + 'Display' => 'always', + 'Required' => 'false', + 'Mask' => 'false', + ); + } + } + } + + return $out; +} + +function xmlToCommand($xml) { + global $var; + $xml = xmlToVar($xml); + $cmdName = (strlen($xml['Name'])) ? '--name="' . $xml['Name'] . '"' : ""; + $cmdPrivileged = (strtolower($xml['Privileged']) == 'true') ? '--privileged="true"' : ""; + $cmdNetwork = '--net="'.strtolower($xml['Network']).'"'; + $Volumes = array(''); + $Ports = array(''); + $Variables = array(''); + $Devices = array(''); + # Bind Time + $Variables[] = 'TZ="' . $var['timeZone'] . '"'; + # Add HOST_OS variable + $Variables[] = 'HOST_OS="unRAID"'; + + foreach ($xml['Config'] as $key => $config) { + $confType = strtolower(strval($config['Type'])); + $hostConfig = strlen($config['Value']) ? $config['Value'] : $config['Default']; + $containerConfig = strval($config['Target']); + $Mode = strval($config['Mode']); + if (! strlen($containerConfig)) continue; + if ($confType == "path") { + $Volumes[] = sprintf( '"%s":"%s":%s', $hostConfig, $containerConfig, $Mode); + } elseif ($confType == 'port') { + $Ports[] = sprintf("%s:%s/%s", $hostConfig, $containerConfig, $Mode); + } elseif ($confType == "variable") { + $Variables[] = sprintf('%s="%s"', $containerConfig, $hostConfig); + } elseif ($confType == "device") { + $Devices[] = '"'.$containerConfig.'"'; + } + } + $cmd = sprintf('/plugins/dynamix.docker.manager/scripts/docker run -d %s %s %s %s %s %s %s %s %s', + $cmdName, + $cmdNetwork, + $cmdPrivileged, + implode(' -e ', $Variables), + implode(' -p ', $Ports), + implode(' -v ', $Volumes), + implode(' --device=', $Devices), + $xml['ExtraParams'], + $xml['Repository']); + + $cmd = preg_replace('/\s+/', ' ', $cmd); + return array($cmd, $xml['Name'], $xml['Repository']); +} + +function getXmlVal($xml, $element, $attr=null, $pos=0) { + $xml = (is_file($xml)) ? simplexml_load_file($xml) : simplexml_load_string($xml); + $element = $xml->xpath("//$element")[$pos]; + return isset($element) ? (isset($element[$attr]) ? strval($element[$attr]) : strval($element)) : ""; +} + +function setXmlVal(&$xml, $value, $el, $attr=null, $pos=0) { + global $echo; + $xml = (is_file($xml)) ? simplexml_load_file($xml) : simplexml_load_string($xml); + $element = $xml->xpath("//$el")[$pos]; + if (! isset($element)) $element = $xml->addChild($el); + if ($attr) { + $element[$attr] = $value; + } else { + $element->{0} = $value; + } + $dom = new DOMDocument('1.0'); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + $xml = $dom->saveXML(); +} + + +# ██████╗ ██████╗ ██████╗ ███████╗ +# ██╔════╝██╔═══██╗██╔══██╗██╔════╝ +# ██║ ██║ ██║██║ ██║█████╗ +# ██║ ██║ ██║██║ ██║██╔══╝ +# ╚██████╗╚██████╔╝██████╔╝███████╗ +# ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ + +## +## CREATE CONTAINER +## + +if (isset($_POST['contName'])) { $postXML = postToXML($_POST, TRUE); // Get the command line list($cmd, $Name, $Repository) = xmlToCommand($postXML); + // Run dry + if ($_POST['dryRun'] == "true") { + echo "

XML

"; + echo "
".htmlentities($postXML)."
"; + echo "

COMMAND:

"; + echo "
".htmlentities($cmd)."
"; + echo '

'; + goto END; + } + + // Will only pull image if it's absent + if (! ImageExist($Repository)) { + // Pull image + pullImage($Repository); + } + // Saving the generated configuration file. $userTmplDir = $dockerManPaths['templates-user']; if(is_dir($userTmplDir) === FALSE){ mkdir($userTmplDir, 0777, true); } - if(strlen($Name)) { $filename = sprintf('%s/my-%s.xml', $userTmplDir, $Name); file_put_contents($filename, $postXML); } - // Pull image - pullImage($Repository); - // Remove existing container if (ContainerExist($Name)){ + // attempt graceful stop of container first + $_GET['cmd'] = "/plugins/dynamix.docker.manager/scripts/docker stop $Name"; + include($dockerManPaths['plugin'] . "/include/Exec.php"); + + // force kill container if still running after 30 seconds $_GET['cmd'] = "/plugins/dynamix.docker.manager/scripts/docker rm -f $Name"; include($dockerManPaths['plugin'] . "/include/Exec.php"); } @@ -285,6 +416,11 @@ if ($_POST){ // Remove old container if renamed $existing = isset($_POST['existingContainer']) ? $_POST['existingContainer'] : FALSE; if ($existing && ContainerExist($existing)){ + // attempt graceful stop of container first + $_GET['cmd'] = "/plugins/dynamix.docker.manager/scripts/docker stop $existing"; + include($dockerManPaths['plugin'] . "/include/Exec.php"); + + // force kill container if still running after 30 seconds $_GET['cmd'] = "/plugins/dynamix.docker.manager/scripts/docker rm -f $existing"; include($dockerManPaths['plugin'] . "/include/Exec.php"); } @@ -293,14 +429,17 @@ if ($_POST){ $_GET['cmd'] = $cmd; include($dockerManPaths['plugin'] . "/include/Exec.php"); - $DockerTemplates->removeInfo($Name); - $DockerUpdate->syncVersions($Name); + // Force information reload + $DockerTemplates->removeInfo($Name, $Repository); + $DockerUpdate->reloadUpdateStatus($Repository); echo '

'; - die(); + goto END; } - +## +## UPDATE CONTAINER +## if ($_GET['updateContainer']){ foreach ($_GET['ct'] as $value) { $Name = urldecode($value); @@ -311,257 +450,504 @@ if ($_GET['updateContainer']){ continue; } - $doc = new DOMDocument('1.0', 'utf-8'); - $doc->preserveWhiteSpace = false; - $doc->load( $tmpl ); - $doc->formatOutput = TRUE; - - $Repository = $doc->getElementsByTagName( "Repository" )->item(0)->nodeValue; - $Registry = $doc->getElementsByTagName( "Registry" )->item(0)->nodeValue; + $xml = file_get_contents($tmpl); + list($cmd, $Name, $Repository) = xmlToCommand($tmpl); + $Registry = getXmlVal($xml, "Registry"); readfile("/usr/local/emhttp/plugins/dynamix.docker.manager/log.htm"); echo ""; @flush(); - $CurrentVersion = $DockerUpdate->getRemoteVersion($Registry, $Repository); - - if ($CurrentVersion){ - if ( $doc->getElementsByTagName( "Version" )->length == 0 ) { - $root = $doc->getElementsByTagName( "Container" )->item(0); - $Version = $root->appendChild($doc->createElement('Version')); - } else { - $Version = $doc->getElementsByTagName( "Version" )->item(0); - } - $Version->nodeValue = $CurrentVersion; - - file_put_contents($tmpl, $doc->saveXML()); - } - $oldContainerID = $DockerClient->getImageID($Repository); - list($cmd, $Name, $Repository) = xmlToCommand($doc->saveXML()); // Pull image flush(); pullImage($Repository); + // attempt graceful stop of container first + $_GET['cmd'] = "/plugins/dynamix.docker.manager/scripts/docker stop $Name"; + include($dockerManPaths['plugin'] . "/include/Exec.php"); + + // force kill container if still running after 10 seconds $_GET['cmd'] = "/plugins/dynamix.docker.manager/scripts/docker rm -f $Name"; include($dockerManPaths['plugin'] . "/include/Exec.php"); $_GET['cmd'] = $cmd; include($dockerManPaths['plugin'] . "/include/Exec.php"); - $DockerTemplates->removeInfo($Name); $newContainerID = $DockerClient->getImageID($Repository); if ( $oldContainerID and $oldContainerID != $newContainerID){ $_GET['cmd'] = sprintf("/plugins/dynamix.docker.manager/scripts/docker rmi %s", $oldContainerID); include($dockerManPaths['plugin'] . "/include/Exec.php"); } - $DockerTemplates->removeInfo($Name); - $DockerUpdate->syncVersions($Name); + // Force information reload + $DockerTemplates->removeInfo($Name, $Repository); + $DockerUpdate->reloadUpdateStatus($Repository); } echo '

'; - die(); + goto END; } +## +## REMOVE TEMPLATE +## if($_GET['rmTemplate']){ unlink($_GET['rmTemplate']); } -if($_GET['xmlTemplate']){ +## +## LOAD TEMPLATE +## + +if ($_GET['xmlTemplate']) { list($xmlType, $xmlTemplate) = split(':', urldecode($_GET['xmlTemplate'])); if(is_file($xmlTemplate)){ - $doc = new DOMDocument(); - $doc->load($xmlTemplate); - - $templateRepository = $doc->getElementsByTagName( "Repository" )->item(0)->nodeValue; - $templateName = $doc->getElementsByTagName( "Name" )->item(0)->nodeValue; - $Registry = $doc->getElementsByTagName( "Registry" )->item(0)->nodeValue; - $templatePrivileged = (strtolower($doc->getElementsByTagName( "Privileged" )->item(0)->nodeValue) == 'true') ? 'checked' : ""; - $templateMode = $doc->getElementsByTagName( "Mode" )->item(0)->nodeValue;; - $readonly = ($xmlType == 'default') ? 'readonly="readonly"' : ''; - $required = ($xmlType == 'default') ? 'required' : ''; - $disabled = ($xmlType == 'default') ? 'disabled="disabled"' : ''; - - if ( $doc->getElementsByTagName( "Description" )->length > 0 ) { - $templateDescription = $doc->getElementsByTagName( "Description" )->item(0)->nodeValue; - } else { - $templateDescription = $DockerTemplates->getTemplateValue($templateRepository, "Description", "default"); - } - - if ( $doc->getElementsByTagName( "Registry" )->length > 0 ) { - $templateRegistry = $doc->getElementsByTagName( "Registry" )->item(0)->nodeValue; - } else { - $templateRegistry = $DockerTemplates->getTemplateValue($templateRepository, "Registry", "default"); - } - - if ( $doc->getElementsByTagName( "WebUI" )->length > 0 ) { - $templateWebUI = $doc->getElementsByTagName( "WebUI" )->item(0)->nodeValue; - } else { - $templateWebUI = $DockerTemplates->getTemplateValue($templateRepository, "WebUI", "default"); - } - - if ( $doc->getElementsByTagName( "Banner" )->length > 0 ) { - $templateBanner = $doc->getElementsByTagName( "Banner" )->item(0)->nodeValue; - } else { - $templateBanner = $DockerTemplates->getTemplateValue($templateRepository, "Banner", "default"); - } - - if ( $doc->getElementsByTagName( "Icon" )->length > 0 ) { - $templateIcon = $doc->getElementsByTagName( "Icon" )->item(0)->nodeValue; - } else { - $templateIcon = $DockerTemplates->getTemplateValue($templateRepository, "Icon", "default"); - } - - if ( $doc->getElementsByTagName( "ExtraParams" )->length > 0 ) { - $templateExtraParams = $doc->getElementsByTagName( "ExtraParams" )->item(0)->nodeValue; - } else { - $templateExtraParams = $DockerTemplates->getTemplateValue($templateRepository, "ExtraParams", "default"); - } - - $templateDescription = stripslashes($templateDescription); - $templateRegistry = stripslashes($templateRegistry); - $templateWebUI = stripslashes($templateWebUI); - $templateBanner = stripslashes($templateBanner); - $templateIcon = stripslashes($templateIcon); - $templateExtraParams = stripslashes($templateExtraParams); - - $templateDescBox = preg_replace('/\[/', '<', $templateDescription); - $templateDescBox = preg_replace('/\]/', '>', $templateDescBox); - - $templatePorts = ''; - $row = ' - - - - - - - - - - - - - - '; - - $i = 1; - foreach($doc->getElementsByTagName('Port') as $port){ - $j = $i + 100; - $ContainerPort = $port->getElementsByTagName( "ContainerPort" )->item(0)->nodeValue; - if (! strlen($ContainerPort)){ continue; } - $HostPort = $port->getElementsByTagName( "HostPort" )->item(0)->nodeValue; - $Protocol = $port->getElementsByTagName( "Protocol" )->item(0)->nodeValue; - $select = ($Protocol == 'udp') ? 'selected' : ''; - $templatePorts .= sprintf($row, $j, htmlspecialchars($ContainerPort), $readonly, htmlspecialchars($HostPort), $required, $select, $j, $disabled); - $i++; - } - - $templateVolumes = ''; - $row = ' - - - - - - -
- - - - - - - - '; - - $i = 1; - foreach($doc->getElementsByTagName('Volume') as $volume){ - $j = $i + 100; - $ContainerDir = $volume->getElementsByTagName( "ContainerDir" )->item(0)->nodeValue; - if (! strlen($ContainerDir)){ continue; } - $HostDir = $volume->getElementsByTagName( "HostDir" )->item(0)->nodeValue; - $Mode = $volume->getElementsByTagName( "Mode" )->item(0)->nodeValue; - $Mode = ($Mode == "ro") ? "selected" : ''; - $templateVolumes .= sprintf($row, $j, htmlspecialchars($ContainerDir), $j, $readonly, $j, htmlspecialchars($HostDir), $j, $required, $j, $Mode, $j, $disabled); - $i++; - } - - $templateVariables = ''; - $row = ' - - - - - - - - - '; - - $i = 1; - foreach($doc->getElementsByTagName('Variable') as $variable){ - $j = $i + 100; - $VariableName = $variable->getElementsByTagName( "Name" )->item(0)->nodeValue; - if (! strlen($VariableName)){ continue; } - $VariableValue = $variable->getElementsByTagName( "Value" )->item(0)->nodeValue; - $templateVariables .= sprintf($row, $j, htmlspecialchars($VariableName), $readonly, htmlspecialchars($VariableValue), $required, $j, $disabled); - $i++; + $xml = xmlToVar($xmlTemplate); + $templateName = $xml["Name"]; + if ($xmlType == "default") { + if (!empty($dockercfg["DOCKER_APP_CONFIG_PATH"]) && file_exists($dockercfg["DOCKER_APP_CONFIG_PATH"])) { + // override /config + foreach ($xml['Config'] as &$arrConfig) { + if ($arrConfig['Type'] == 'Path' && strtolower($arrConfig['Target']) == '/config') { + $arrConfig['Default'] = $arrConfig['Value'] = realpath($dockercfg["DOCKER_APP_CONFIG_PATH"]).'/'.$xml["Name"]; + $arrConfig['Display'] = 'hidden'; + } + } + } + if (!empty($dockercfg["DOCKER_APP_UNRAID_PATH"]) && file_exists($dockercfg["DOCKER_APP_UNRAID_PATH"])) { + // override /unraid + $boolFound = false; + foreach ($xml['Config'] as &$arrConfig) { + if ($arrConfig['Type'] == 'Path' && strtolower($arrConfig['Target']) == '/unraid') { + $arrConfig['Default'] = $arrConfig['Value'] = realpath($dockercfg["DOCKER_APP_UNRAID_PATH"]); + $arrConfig['Display'] = 'hidden'; + $boolFound = true; + } + } + if (!$boolFound) { + $xml['Config'][] = array('Name' => 'unRAID Share Path', + 'Target' => '/unraid', + 'Default' => realpath($dockercfg["DOCKER_APP_UNRAID_PATH"]), + 'Value' => realpath($dockercfg["DOCKER_APP_UNRAID_PATH"]), + 'Mode' => 'rw', + 'Description' => '', + 'Type' => 'Path', + 'Display' => 'hidden', + 'Required' => 'false', + 'Mask' => 'false', + ); + } + } } + $xml['Description'] = str_replace(array('[', ']'), array('<', '>'), $xml['Overview']); + echo ""; } } -$showAdditionalInfo = true; +$showAdditionalInfo = ''; ?> - - - + + + + + + + +
+
-
-
+ +
\n"; endif; + if (ContainerExist($templateName)): echo "\n"; endif; else:?> - - + - - + - - - - + > + + - - - - - - - - > + - - - - - + + + - - + + + + > + + + + > + - - - - - + > + + - - > + - - + > - - + - - > + - - - - - - - - +
Template: - Template: +

Templates are a quicker way to setting up Docker Containers on your unRAID server. There are two types of templates:

@@ -615,326 +1000,330 @@ table td{font-size:14px;vertical-align:bottom;text-align:left;}
Description: -
-
Container Page: " . htmlspecialchars($Registry) . ""; - } - ?> -
-
Name:
Name:

Give the container a name or leave it as default.

Overview:
Overview:
Repository:

The repository for the application on the Docker Registry. Format of authorname/appname. Optionally you can add a : after appname and request a specific version for the container image.

Network type:
Network Type: + +

If the Bridge type is selected, the application’s network access will be restricted to only communicating on the ports specified in the port mappings section. If the Host type is selected, the application will be given access to communicate using any port on the host that isn’t already mapped to another in-use application/service. Generally speaking, it is recommended to leave this setting to its default value as specified per application template.

IMPORTANT NOTE: If adjusting port mappings, do not modify the settings for the Container port as only the Host port can be adjusted.

Privileged:> +

For containers that require the use of host-device access directly or need full exposure to host capabilities, this option will need to be selected. For more information, see this link: https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration

+ +
+ + + +
 
-
- Volume Mappings -
- - - - - - - - - - - - - - - - - - - -
Container volume:Host path:Access:
- - - -
-
- - -
- -
-

Applications can be given read and write access to your data by mapping a directory path from the container to a directory path on the host. When looking at the volume mappings section, the Container volume represents the path from the container that will be mapped. The Host path represents the path the Container volume will map to on your unRAID system. All applications should require at least one volume mapping to store application metadata (e.g., media libraries, application settings, user profile data, etc.). Clicking inside these fields provides a "picker" that will let you navigate to where the mapping should point. Additional mappings can be manually created by clicking the Add Path button. Most applications will need you to specify additional mappings in order for the application to interact with other data on the system (e.g., with Plex Media Server, you should specify an additional mapping to give it access to your media files). It is important that when naming Container volumes that you specify a path that won’t conflict with already existing folders present in the container. If unfamiliar with Linux, using a prefix such as "unraid_" for the volume name is a safe bet (e.g., "/unraid_media" is a valid Container volume name).

-
- -
-
- Port Mappings -
- - - - - - - - - - - - - - - - - -
Container port:Host port:Protocol:
- - - - - - - -
- -
-

When the network type is set to Bridge, you will be given the option of customizing what ports the container will use. While applications may be configured to talk to a specific port by default, we can remap those to different ports on our host with Docker. This means that while three different apps may all want to use port 8000, we can map each app to a unique port on the host (e.g., 8000, 8001, and 8002). When the network type is set to Host, the container will be allowed to use any available port on your system. Additional port mappings can be created, similar to Volumes, although this is not typically necessary when working with templates as port mappings should already be specified.

-
-
- - - -
> -
- Extra Parameters -
- - - -
-

If you wish to append additional commands to your Docker container at run-time, you can specify them here. For example, if you wish to pin an application to live on a specific CPU core, you can enter "--cpuset=0" in this field. Change 0 to the core # on your system (starting with 0). You can pin multiple cores by separation with a comma or a range of cores by separation with a dash. For all possible Docker run-time commands, see here: https://docs.docker.com/reference/run/

-
-
- -
style="display:none"> -
+
+ + + - - - - + + + + + + + + + + + + + + + + - - - - - + + - - - - - - - - - - - - - - + + + + - - + + + + +
Docker Hub URL: - + +
+

A description for the application container. Supports basic HTML mark-up.

+
Categories:
Support Thread:
+
+

Lnk to a support thread on Lime-Technology's forum.

+
+
Docker Hub URL:

The path to the container's repository location on the Docker Hub.

WebUI: - -
Banner: - -
Icon: - -
Description: - + WebUI:
+
+

When you click on an application icon from the Docker Containers page, the WebUI option will link to the path in this field. Use [IP} to identify the IP of your host and [PORT:####] replacing the #'s for your port.

+
Extra Parameters:
-

A description for the application container. Supports basic HTML mark-up.

+

If you wish to append additional commands to your Docker container at run-time, you can specify them here. For example, if you wish to pin an application to live on a specific CPU core, you can enter "--cpuset=0" in this field. Change 0 to the core # on your system (starting with 0). You can pin multiple cores by separation with a comma or a range of cores by separation with a dash. For all possible Docker run-time commands, see here: https://docs.docker.com/reference/run/


-
- - + + + + + +
  + + + +
+


+
+ + + + - - - + diff --git a/plugins/dynamix.docker.manager/include/DockerClient.php b/plugins/dynamix.docker.manager/include/DockerClient.php index 17bd90d25..53eced5f7 100644 --- a/plugins/dynamix.docker.manager/include/DockerClient.php +++ b/plugins/dynamix.docker.manager/include/DockerClient.php @@ -55,6 +55,29 @@ if (! isset($var)){ $var = @parse_ini_file("/usr/local/emhttp/state/var.ini"); } +$docker_cfg_defaults = [ + "DOCKER_ENABLED" => "no", + "DOCKER_OPTS" => "--storage-driver=btrfs", + "DOCKER_IMAGE_SIZE" => "20", + "DOCKER_IMAGE_FILE" => "/mnt/user/system/docker/docker.img", + "DOCKER_APP_CONFIG_PATH" => "/mnt/user/appdata/", + "DOCKER_APP_UNRAID_PATH" => "" +]; +$dockercfg = $docker_cfg_defaults; + +// Docker configuration file - create if needed +$docker_cfgfile = "/boot/config/docker.cfg"; +if (!file_exists($docker_cfgfile)) { + $tmp = ''; + foreach ($docker_cfg_defaults as $key => $value) $tmp .= "$key=\"$value\"\n"; + file_put_contents($docker_cfgfile, $tmp); +} else { + $docker_cfg_existing = parse_ini_file($docker_cfgfile); + if (!empty($docker_cfg_existing)) { + $dockercfg = array_merge($docker_cfg_defaults, $docker_cfg_existing); + } +} + ###################################### ## DOCKERTEMPLATES CLASS ## ###################################### @@ -67,10 +90,10 @@ class DockerTemplates { if($this->verbose) echo $m."\n"; } - public function download_url($url, $path = "", $bg = FALSE){ - exec("curl --max-time 60 --silent --insecure --location --fail ".($path ? " -o " . escapeshellarg($path) : "")." " . escapeshellarg($url) . " ".($bg ? ">/dev/null 2>&1 &" : "2>/dev/null"), $out, $exit_code ); - return ($exit_code === 0 ) ? implode("\n", $out) : FALSE; - } + public function download_url($url, $path = "", $bg = FALSE){ + exec("curl --max-time 60 --silent --insecure --location --fail ".($path ? " -o " . escapeshellarg($path) : "")." " . escapeshellarg($url) . " ".($bg ? ">/dev/null 2>&1 &" : "2>/dev/null"), $out, $exit_code ); + return ($exit_code === 0 ) ? implode("\n", $out) : FALSE; + } public function listDir($root, $ext=NULL) { $iter = new RecursiveIteratorIterator( @@ -133,10 +156,10 @@ class DockerTemplates { $repotemplates = array(); $output = ""; $tmp_dir = "/tmp/tmp-".mt_rand(); - if (!file_exists($dockerManPaths['template-repos'])) { - @mkdir(dirname($dockerManPaths['template-repos']), 0777, true); - @file_put_contents($dockerManPaths['template-repos'], "https://github.com/limetech/docker-templates"); - } + if (!file_exists($dockerManPaths['template-repos'])) { + @mkdir(dirname($dockerManPaths['template-repos']), 0777, true); + @file_put_contents($dockerManPaths['template-repos'], "https://github.com/limetech/docker-templates"); + } $urls = @file($Urls, FILE_IGNORE_NEW_LINES); if ( ! is_array($urls)) return false; $this->debug("\nURLs:\n " . implode("\n ", $urls)); @@ -275,8 +298,9 @@ class DockerTemplates { } - public function removeInfo($container){ + public function removeInfo($container, $image){ global $dockerManPaths; + $image = ($image && count(preg_split("#[:\/]#", $image)) < 3) ? "${image}:latest" : $image; $dockerIni = $dockerManPaths['webui-info']; if (! is_dir( dirname( $dockerIni ))) @mkdir( dirname( $dockerIni ), 0770, true); $info = (is_file($dockerIni)) ? json_decode(file_get_contents($dockerIni), TRUE) : array(); @@ -287,7 +311,7 @@ class DockerTemplates { $update_file = $dockerManPaths['update-status']; $updateStatus = (is_file($update_file)) ? json_decode(file_get_contents($update_file), TRUE) : array(); - if (isset($updateStatus[$container])) unset($updateStatus[$container]); + if (isset($updateStatus[$image])) unset($updateStatus[$image]); file_put_contents($update_file, json_encode($updateStatus, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } @@ -296,6 +320,7 @@ class DockerTemplates { global $dockerManPaths; $DockerClient = new DockerClient(); $DockerUpdate = new DockerUpdate(); + $DockerUpdate->verbose = $this->verbose; $new_info = array(); $dockerIni = $dockerManPaths['webui-info']; @@ -310,9 +335,6 @@ class DockerTemplates { $allAutoStart = @file($autostart_file, FILE_IGNORE_NEW_LINES); if ($allAutoStart===FALSE) $allAutoStart = array(); - $update_file = $dockerManPaths['update-status']; - $updateStatus = (is_file($update_file)) ? json_decode(file_get_contents($update_file), TRUE) : array(); - foreach ($containers as $ct) { $name = $ct['Name']; $image = $ct['Image']; @@ -321,34 +343,33 @@ class DockerTemplates { $tmp['running'] = $ct['Running']; $tmp['autostart'] = in_array($name, $allAutoStart); - $img = $this->getBannerIcon( $image ); - $tmp['banner'] = ( $img['banner'] ) ? $img['banner'] : "#"; - $tmp['icon'] = ( $img['icon'] ) ? $img['icon'] : "#"; - - $WebUI = $this->getControlURL($name); - $tmp['url'] = ($WebUI) ? $WebUI : "#"; + if (! $tmp['icon'] || ! $tmp['banner'] || $reload) { + $img = $this->getBannerIcon( $image ); + $tmp['banner'] = ( $img['banner'] ) ? $img['banner'] : null; + $tmp['icon'] = ( $img['icon'] ) ? $img['icon'] : null; + } + if (! $tmp['url'] || $reload) { + $WebUI = $this->getControlURL($name); + $tmp['url'] = ($WebUI) ? $WebUI : null; + } $Registry = $this->getTemplateValue($image, "Registry"); - $tmp['registry'] = ( $Registry ) ? $Registry : "#"; + $tmp['registry'] = ( $Registry ) ? $Registry : null; - if ($reload) { - $nv = $DockerUpdate->getUpdateStatus($name, $image); - if ($nv != 'undef'){ - $updateStatus[$name] = $nv; - } + if (! $tmp['updated'] || $reload) { + if ($reload) $DockerUpdate->reloadUpdateStatus($image); + $vs = $DockerUpdate->getUpdateStatus($image); + $tmp['updated'] = ($vs === NULL) ? null : ( ($vs === TRUE) ? 'true' : 'false' ); } - $tmp['updated'] = (array_key_exists($name, $updateStatus)) ? $updateStatus[$name] : 'undef'; - $tmp['template'] = $this->getUserTemplate($name); + if (! $tmp['template'] || $reload){ + $tmp['template'] = $this->getUserTemplate($name); + } - $this->debug("\n$name");foreach ($tmp as $c => $d) $this->debug(sprintf(" %-10s: %s", $c, $d)); + $this->debug("\n$name");foreach ($tmp as $c => $d) $this->debug(sprintf(" %-10s: %s", $c, $d)); $new_info[$name] = $tmp; } file_put_contents($dockerIni, json_encode($new_info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - if($reload) { - foreach ($updateStatus as $ct => $update) if (!isset($new_info[$ct])) unset($updateStatus[$ct]); - file_put_contents($update_file, json_encode($updateStatus, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } return $new_info; } @@ -358,12 +379,9 @@ class DockerTemplates { $out = array(); $Images = array(); - $Images = array('banner' => $this->getTemplateValue($Repository, "Banner"), + $Images = array('banner' => $this->getTemplateValue($Repository, "Banner"), 'icon' => $this->getTemplateValue($Repository, "Icon") ); - $defaultImages = array('banner' => '/plugins/dynamix.docker.manager/images/spacer.png', - 'icon' => '/plugins/dynamix.docker.manager/images/question.png'); - foreach ($Images as $type => $imgUrl) { preg_match_all("/(.*?):([\w]*$)/i", $Repository, $matches); $tempPath = sprintf("%s/%s-%s-%s.png", $dockerManPaths[ 'images-ram' ], preg_replace('%\/|\\\%', '-', $matches[1][0]), $matches[2][0], $type); @@ -389,6 +407,12 @@ class DockerTemplates { ## DOCKERUPDATE CLASS ## ###################################### class DockerUpdate{ + public $verbose = false; + + private function debug($m) { + if($this->verbose) echo $m."\n"; + } + public function download_url($url, $path = "", $bg = FALSE){ exec("curl --max-time 30 --silent --insecure --location --fail ".($path ? " -o " . escapeshellarg($path) : "")." " . escapeshellarg($url) . " ".($bg ? ">/dev/null 2>&1 &" : "2>/dev/null"), $out, $exit_code ); @@ -396,37 +420,58 @@ class DockerUpdate{ } - public function getRemoteVersion($RegistryUrl, $image){ - preg_match_all("/:([\w]*$)/i", $image, $matches); - $tag = isset($matches[1][0]) ? $matches[1][0] : "latest"; - preg_match("#/u/([^/]*)/([^/]*)#", $RegistryUrl, $matches); - $apiUrl = sprintf("http://index.docker.io/v1/repositories/%s/%s/tags/%s", $matches[1], $matches[2], $tag); + public function getRemoteVersion($image){ + $apiUrl = vsprintf("http://index.docker.io/v1/repositories/%s/%s/tags/%s", preg_split("#[:\/]#", $image)); + $this->debug("API URL: $apiUrl"); $apiContent = $this->download_url($apiUrl); - return ( $apiContent === FALSE ) ? NULL : substr(json_decode($apiContent, TRUE)[0]['id'],0,16); + return ( $apiContent === FALSE ) ? NULL : substr(json_decode($apiContent, TRUE)[0]['id'],0,8); } - public function getLocalVersion($file){ - if(is_file($file)){ - $doc = new DOMDocument(); - $doc->load($file); - if ( ! $doc->getElementsByTagName( "Version" )->length == 0 ) { - return $doc->getElementsByTagName( "Version" )->item(0)->nodeValue; + public function getLocalVersion($image){ + $DockerClient = new DockerClient(); + return substr($DockerClient->getImageID($image), 0, 8); + } + + + public function getUpdateStatus($image) { + global $dockerManPaths; + $DockerClient = new DockerClient(); + $update_file = $dockerManPaths['update-status']; + $updateStatus = (is_file($update_file)) ? json_decode(file_get_contents($update_file), TRUE) : array(); + // Add :latest tag to image if it's absent + $image = ($image && count(preg_split("#[:\/]#", $image)) < 3) ? "${image}:latest" : $image; + if(isset($updateStatus[$image])) { + if ($updateStatus[$image]['local'] && $updateStatus[$image]['remote']) { + return ($updateStatus[$image]['local'] == $updateStatus[$image]['remote']) ? true : false; } else { - return NULL; + return null; } + } else { + return null; } } - public function getUpdateStatus($container, $image) { - $DockerTemplates = new DockerTemplates(); - $RegistryUrl = $DockerTemplates->getTemplateValue($image, "Registry"); - $userFile = $DockerTemplates->getUserTemplate($container); - $localVersion = $this->getLocalVersion($userFile); - $remoteVersion = $this->getRemoteVersion($RegistryUrl, $image); - // echo "\n $localVersion => $remoteVersion"; - return ($localVersion && $remoteVersion) ? (($remoteVersion == $localVersion) ? "true" : "false") : "undef" ; + public function reloadUpdateStatus($image = null) { + global $dockerManPaths; + $DockerClient = new DockerClient(); + $update_file = $dockerManPaths['update-status']; + $updateStatus = (is_file($update_file)) ? json_decode(file_get_contents($update_file), TRUE) : array(); + + // Add :latest tag to image if it's absent + $image = ($image && count(preg_split("#[:\/]#", $image)) < 3) ? "${image}:latest" : $image; + $images = ($image) ? array($image) : array_map(function($ar){return $ar['Tags'][0];}, $DockerClient->getDockerImages()); + foreach ($images as $img) { + $localVersion = $this->getLocalVersion($img); + $remoteVersion = $this->getRemoteVersion($img); + $status = ($localVersion && $remoteVersion) ? (($remoteVersion == $localVersion) ? "true" : "false") : "undef"; + $updateStatus[$img] = array('local' => $localVersion, + 'remote' => $remoteVersion, + 'status' => $status); + $this->debug("Update status: Image='${img}', Local='${localVersion}', Remote='${remoteVersion}'"); + } + file_put_contents($update_file, json_encode($updateStatus, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } @@ -445,6 +490,12 @@ class DockerUpdate{ ###################################### class DockerClient { + private $allContainersCache = null; + + + private $allImagesCache = null; + + private function build_sorter($key) { return function ($a, $b) use ($key) { return strnatcmp(strtolower($a[$key]), strtolower($b[$key])); @@ -486,65 +537,114 @@ class DockerClient { } - private function getDockerJSON($url, $method = "GET"){ + public function getDockerJSON($url, $method = "GET", &$code = null, $callback = null, $unchunk = false){ $fp = stream_socket_client('unix:///var/run/docker.sock', $errno, $errstr); if ($fp === false) { echo "Couldn't create socket: [$errno] $errstr"; return NULL; } - $out="$method {$url} HTTP/1.1\r\nConnection: Close\r\n\r\n"; + $protocol = ($unchunk) ? "HTTP/1.0" : "HTTP/1.1"; + $out="${method} {$url} ${protocol}\r\nConnection: Close\r\n\r\n"; fwrite($fp, $out); // Strip headers out + $headers = ''; while (($line = fgets($fp)) !== false) { + if (! is_bool(strpos($line, "HTTP/1"))) { + $code = vsprintf('%2$s',preg_split("#\s+#", $line)); + } + $headers .= $line; if (rtrim($line) == '') { break; } } - $data = ''; + $data = array(); while (($line = fgets($fp)) !== false) { - $data .= $line; + if (is_array($j = json_decode($line, true))) { + $data = array_merge($data, $j); + } + if ($callback) $callback($line); } fclose($fp); - $data = $this->unchunk($data); - $json = json_decode( $data, true ); - if ($json === null) { - $json = array(); - } else if (!array_key_exists(0, $json) && !empty($json)) { - $json = [ $json ]; - } - return $json; + return $data; + } + + + public function createDockerContainer() { + return false; + } public function getInfo(){ $info = $this->getDockerJSON("/info"); $version = $this->getDockerJSON("/version"); - return array_merge($info[0], $version[0]); + return array_merge($info, $version); } - private function getContainerDetails($id){ + public function getContainetLog($id, $callback, $tail = null, $since = null) { + $this->getDockerJSON("/containers/${id}/logs?stderr=1&stdout=1&tail=${tail}&since=${since}", "GET", $code, $callback, true); + } + + + public function getContainerDetails($id){ $json = $this->getDockerJSON("/containers/{$id}/json"); return $json; } public function startContainer($id){ - $json = $this->getDockerJSON("/containers/${id}/start", "POST"); - return $json; - } - - - public function removeImage($id){ - $json = $this->getDockerJSON("/images/{$id}", "DELETE"); - return $json; + $this->getDockerJSON("/containers/${id}/start", "POST", $code); + $codes = array("204" => "No error", + "304" => "Container already started", + "404" => "No such container", + "500" => "Server error"); + return ($code == "204") ? true : $codes[$code]; } public function stopContainer($id){ - $json = $this->getDockerJSON("/containers/${id}/stop", "POST"); - return $json; + $this->getDockerJSON("/containers/${id}/stop", "POST", $code); + $codes = array("204" => "No error.", + "304" => "Container already started.", + "404" => "No such container.", + "500" => "Server error."); + return ($code == "204") ? true : $codes[$code]; + } + + + public function restartContainer($id){ + $json = $this->getDockerJSON("/containers/${id}/restart", "POST", $code); + $codes = array("204" => "No error.", + "404" => "No such container.", + "500" => "Server error."); + return ($code == "204") ? true : $codes[$code]; + } + + + public function removeContainer($id){ + $json = $this->getDockerJSON("/containers/{$id}?force=1", "DELETE", $code); + $codes = array("204" => "No error.", + "400" => "Bad parameter.", + "404" => "No such container.", + "500" => "Server error."); + return ($code == "204") ? true : $codes[$code]; + } + + + public function pullImage($image, $callback = null) { + return $this->getDockerJSON("/images/create?fromImage=$image", "POST", $code, $callback); + } + + + public function removeImage($id){ + $json = $this->getDockerJSON("/images/{$id}?force=1", "DELETE", $code); + $codes = array("200" => "No error.", + "404" => "No such image.", + "409" => "Conflict: image used by container ".$this->usedBy($id)[0].".", + "500" => "Server error."); + return ($code == "200") ? true : $codes[$code]; } @@ -555,6 +655,11 @@ class DockerClient { public function getDockerContainers(){ + // Return cached values + if (is_array($this->allContainersCache)){ + return $this->allContainersCache; + } + $containers = array(); $json = $this->getDockerJSON("/containers/json?all=1"); @@ -566,24 +671,24 @@ class DockerClient { preg_match("/\b^Up\b/", $status, $matches); $running = $matches ? TRUE : FALSE; $details = $this->getContainerDetails($obj['Id']); + // echo "
".print_r($obj,TRUE)."
"; - // echo "
".print_r($details,TRUE)."
"; - + // Docker 1.7 uses full image ID when there aren't tags, so lets crop it + $Image = (strlen($obj['Image']) == 64) ? substr($obj['Image'],0,12) : $obj['Image']; // Docker 1.7 doesn't automatically append the tag 'latest', so we do that now if there's no tag - preg_match_all("/:([\w]*$)/i", $obj['Image'], $matches2); - - $c["Image"] = $obj['Image'] . (isset($matches2[1][0]) ? "" : ":latest"); - $c["ImageId"] = substr($details[0]["Image"],0,12); - $c["Name"] = substr($details[0]['Name'], 1); + $c["Image"] = ($Image && count(preg_split("#[:\/]#", $Image)) < 3) ? "${Image}:latest" : $Image; + $c["ImageId"] = substr($details["Image"],0,12); + $c["Name"] = substr($details['Name'], 1); $c["Status"] = $status; $c["Running"] = $running; $c["Cmd"] = $obj['Command']; $c["Id"] = substr($obj['Id'],0,12); - $c['Volumes'] = $details[0]["HostConfig"]['Binds']; + $c['Volumes'] = $details["HostConfig"]['Binds']; $c["Created"] = $this->humanTiming($obj['Created']); - $c["NetworkMode"] = $details[0]['HostConfig']['NetworkMode']; + $c["NetworkMode"] = $details['HostConfig']['NetworkMode']; + $c["BaseImage"] = isset($obj["Labels"]["BASEIMAGE"]) ? $obj["Labels"]["BASEIMAGE"] : false; - $Ports = $details[0]['HostConfig']['PortBindings']; + $Ports = $details['HostConfig']['PortBindings']; $Ports = (count ( $Ports )) ? $Ports : array(); $c["Ports"] = array(); if ($c["NetworkMode"] != 'host'){ @@ -599,10 +704,24 @@ class DockerClient { $containers[] = $c; } usort($containers, $this->build_sorter('Name')); + $this->allContainersCache = $containers; + return $containers; } + public function getContainerID($Container){ + $allContainers = $this->getDockerContainers(); + foreach ($allContainers as $ct) { + preg_match("%" . preg_quote($Container, "%") ."%", $ct["Name"], $matches); + if( $matches){ + return $ct["Id"]; + } + } + return NULL; + } + + public function getImageID($Image){ $allImages = $this->getDockerImages(); foreach ($allImages as $img) { @@ -629,7 +748,10 @@ class DockerClient { public function getDockerImages(){ - + // Return cached values + if (is_array($this->allImagesCache)){ + return $this->allImagesCache; + } $images = array(); $c = array(); $json = $this->getDockerJSON("/images/json?all=0"); @@ -642,6 +764,7 @@ class DockerClient { foreach($obj['RepoTags'] as $t){ $tags[] = htmlentities($t); } + // echo "
".print_r($obj,TRUE)."
"; $c["Created"] = $this->humanTiming($obj['Created']);//date('Y-m-d H:i:s', $obj['Created']); $c["Id"] = substr($obj['Id'],0,12); @@ -649,17 +772,12 @@ class DockerClient { $c["Size"] = $this->formatBytes($obj['Size']); $c["VirtualSize"] = $this->formatBytes($obj['VirtualSize']); $c["Tags"] = $tags; + $c["Repository"] = vsprintf('%1$s/%2$s',preg_split("#[:\/]#", $tags[0])); $c["usedBy"] = $this->usedBy($c["Id"]); - $imgDetails = $this->getImageDetails($obj['Id']); - // echo "
".print_r($imgDetails,TRUE)."
"; - $a = $imgDetails[0]['Config']['Volumes']; - $b = $imgDetails[0]['Config']['ExposedPorts']; - $c['ImageType'] = (! count($a) && ! count($b)) ? 'base' : 'user'; - - $images[] = $c; - + $images[$c["Id"]] = $c; } + $this->allImagesCache = $images; return $images; } } diff --git a/plugins/dynamix.docker.manager/include/Events.php b/plugins/dynamix.docker.manager/include/Events.php new file mode 100644 index 000000000..06631a9f6 --- /dev/null +++ b/plugins/dynamix.docker.manager/include/Events.php @@ -0,0 +1,49 @@ + $DockerClient->startContainer($container) )); + break; + case 'stop': + if ($container) echo json_encode(array('success' => $DockerClient->stopContainer($container) )); + break; + case 'restart': + if ($container) echo json_encode(array('success' => $DockerClient->restartContainer($container) )); + break; + case 'remove_container': + if ($container) echo json_encode(array('success' => $DockerClient->removeContainer($container) )); + break; + case 'remove_image': + if ($image) echo json_encode(array('success' => $DockerClient->removeImage($image) )); + break; +} + +$container = $_GET['container']; +$since = $_GET['since']; +$title = $_GET['title']; + +switch ($_GET['action']) { + case 'log': + if ($container) { + $echo = function($s){$s=addslashes(substr(trim($s),8));echo "";@flush();}; + if (!$since) { + readfile("/usr/local/emhttp/plugins/dynamix.docker.manager/log.htm"); + echo ""; + $tail = 350; + } else { + $tail = null; + } + $DockerClient->getContainetLog($container, $echo, $tail, $since); + echo ""; + @flush(); + } + break; + +} +?> + diff --git a/plugins/dynamix.docker.manager/javascript/docker.js b/plugins/dynamix.docker.manager/javascript/docker.js index ca1cd25d4..ba19e0dca 100644 --- a/plugins/dynamix.docker.manager/javascript/docker.js +++ b/plugins/dynamix.docker.manager/javascript/docker.js @@ -1,4 +1,6 @@ -function addDockerContainerContext(container, image, template, started, update, autostart, webui){ +var eventURL = "/plugins/dynamix.docker.manager/include/Events.php"; + +function addDockerContainerContext(container, image, template, started, update, autostart, webui, id){ var opts = [{header: container, image: "/plugins/dynamix.docker.manager/images/dynamix.docker.manager.png"}]; if (started && (webui != "#")) { opts.push({text: 'WebUI', icon:'fa-globe', href: webui, target: '_blank' }); @@ -9,20 +11,20 @@ function addDockerContainerContext(container, image, template, started, update, opts.push({divider: true}); } if (started){ - opts.push({text: 'Stop', icon:'fa-stop', action: function(e){ e.preventDefault(); containerControl(container, 'stop'); }}); - opts.push({text: 'Restart', icon:'fa-refresh', action: function(e){ e.preventDefault(); containerControl(container, 'restart'); }}); + opts.push({text: 'Stop', icon:'fa-stop', action: function(e){ e.preventDefault(); containerControl(id, 'stop', true); }}); + opts.push({text: 'Restart', icon:'fa-refresh', action: function(e){ e.preventDefault(); containerControl(id, 'restart', true); }}); } else { - opts.push({text: 'Start', icon:'fa-play', action: function(e){ e.preventDefault(); containerControl(container, 'start'); }}); + opts.push({text: 'Start', icon:'fa-play', action: function(e){ e.preventDefault(); containerControl(id, 'start', true); }}); } opts.push({divider: true}); if (location.pathname.indexOf("/Dashboard") === 0) { - opts.push({text: 'Logs', icon:'fa-navicon', action: function(e){ e.preventDefault(); containerLogs(container); }}); + opts.push({text: 'Logs', icon:'fa-navicon', action: function(e){ e.preventDefault(); containerLogs(container, id); }}); } if (template) { opts.push({text: 'Edit', icon:'fa-wrench', action: function(e){ e.preventDefault(); editContainer(container, template); }}); } opts.push({divider: true}); - opts.push({text: 'Remove', icon:'fa-trash', action: function(e){ e.preventDefault(); rmContainer(container, image); }}); + opts.push({text: 'Remove', icon:'fa-trash', action: function(e){ e.preventDefault(); rmContainer(container, image, id); }}); context.attach('#context-'+container, opts); } @@ -86,23 +88,9 @@ function editContainer(container, template) { location = path + '/UpdateContainer?xmlTemplate=edit:' + template; } -function rmContainer(containers, images){ - var ctCmd = "/plugins/dynamix.docker.manager/scripts/docker rm -f"; - var imgCmd = "/plugins/dynamix.docker.manager/scripts/docker rmi"; - var ctTitle = ""; - if (typeof containers === "object") { - for (var i = 0; i < containers.length; i++) { - ctCmd += " " + containers[i]; - imgCmd += " " + images[i]; - ctTitle += containers[i] + "
"; - } - } else { - ctCmd += " " + containers; - imgCmd += " " + images; - ctTitle += containers + "
"; - } - var title = 'Removing container'; - $( "#dialog-confirm" ).html(ctTitle); +function rmContainer(container, image, id){ + var title = 'Removing container: '+ container; + $( "#dialog-confirm" ).html(''); $( "#dialog-confirm" ).append( "
Are you sure?" ); $( "#dialog-confirm" ).dialog({ title: title, @@ -114,13 +102,12 @@ function rmContainer(containers, images){ buttons: { "Just the container": function() { $( this ).dialog( "close" ); - var cmd = '/plugins/dynamix.docker.manager/include/Exec.php?cmd=' + encodeURIComponent(ctCmd); - popupWithIframe(title, cmd, true); + containerControl(id, 'remove_container', true); }, "Container and image": function() { $( this ).dialog( "close" ); - var cmd = '/plugins/dynamix.docker.manager/include/Exec.php?cmd=' + encodeURIComponent(ctCmd + ";" + imgCmd); - popupWithIframe(title, cmd, true); + containerControl(id, 'remove_container', false); + imageControl(image, "remove_image", true); }, Cancel: function() { $( this ).dialog( "close" ); @@ -134,20 +121,12 @@ function rmContainer(containers, images){ $(".ui-button-text").css('padding','0px 5px'); } -function updateContainer(containers){ - var ctCmd =""; +function updateContainer(container){ + var ctCmd = "&ct[]=" + encodeURIComponent(container); var ctTitle = ""; - if (typeof containers === "object") { - for (var i = 0; i < containers.length; i++) { - ctCmd += "&ct[]=" + encodeURIComponent(containers[i]); - ctTitle += containers[i] + "
"; - } - } else { - ctCmd += "&ct[]=" + encodeURIComponent(containers); - ctTitle += containers + "
"; - } - var title = 'Updating container'; - $( "#dialog-confirm" ).html(ctTitle); + + var title = 'Updating container: '+container; + $( "#dialog-confirm" ).html(''); $( "#dialog-confirm" ).append( "
Are you sure?" ); $( "#dialog-confirm" ).dialog({ title: title, @@ -175,20 +154,10 @@ function updateContainer(containers){ $( ".ui-dialog .ui-dialog-title" ).css( 'width', "100%"); } -function rmImage(images, imageName){ - var imgCmd = "/plugins/dynamix.docker.manager/scripts/docker rmi"; - var imgTitle = ""; - if (typeof images === "object") { - for (var i = 0; i < images.length; i++) { - imgCmd += " " + images[i]; - imgTitle += imageName[i] + "
"; - } - } else { - imgCmd += " " + images; - imgTitle += imageName + "
"; - } - var title = "Removing image"; - $( "#dialog-confirm" ).html(imgTitle); +function rmImage(image, imageName){ + var imageName = $('