. * */ /** * Helper to manage the Docker container for the Nextcloud test server. * * The NextcloudTestServerDockerHelper abstracts the calls to the Docker Command * Line Interface (the "docker" command) to run, get information from, and * destroy containers. It is not a generic abstraction, but one tailored * specifically to the Nextcloud test server; a Docker image that provides an * installed and ready to run Nextcloud server with the configuration and data * expected by the acceptance tests must be available in the system. The * Nextcloud server must use a local storage so all the changes it makes are * confined to its running container. * * Also, the Nextcloud server installed in the Docker image is expected to see * "127.0.0.1" as a trusted domain (which would be the case if it was installed * by running "occ maintenance:install"). Therefore, the Nextcloud server is * accessed through a local port in the host system mapped to the port 80 of the * Docker container; if the Nextcloud server was instead accessed directly * through its IP address it would complain that it was being accessed from an * untrusted domain and refuse to work until the admin whitelisted it. The IP * address and port to access the Nextcloud server can be got from * "getNextcloudTestServerAddress". * * For better compatibility, Docker CLI commands used internally follow the * pre-1.13 syntax (also available in 1.13 and newer). For example, * "docker start" instead of "docker container start". * * In any case, the "docker" command requires special permissions to talk to the * Docker daemon, and those permissions are typically available only to the root * user. However, you should NOT run the acceptance tests as root, but as a * regular user instead. Please see the Docker documentation to find out how to * give access to a regular user to the Docker daemon: * https://docs.docker.com/engine/installation/linux/linux-postinstall/ * * Note, however, that being able to communicate with the Docker daemon is the * same as being able to get root privileges for the system. Therefore, you must * give access to the Docker daemon (and thus run the acceptance tests as) ONLY * to trusted and secure users: * https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface * * All the public methods that use the 'docker' command throw an exception if * the command can not be executed or if it does not have enough permissions to * connect to the Docker daemon; as, due to the current use of this class, it is * just a warning for the test runner and nothing to be explicitly catched a * plain base Exception is used. */ class NextcloudTestServerDockerHelper { /** * @var string */ private $imageName; /** * @var string */ private $hostPortRangeForContainer; /** * @var string */ private $containerName; /** * Creates a new NextcloudTestServerDockerHelper. * * @param string $imageName the name of the Docker image that provides the * Nextcloud test server. * @param string $hostPortRangeForContainer the range of local ports in the * host in which the port 80 of the container can be published. */ public function __construct($imageName = "nextcloud-local-test-acceptance", $hostPortRangeForContainer = "15000-16000") { $this->imageName = $imageName; $this->hostPortRangeForContainer = $hostPortRangeForContainer; $this->containerName = null; } /** * Creates and starts the container. * * Note that, even if the container has started, the server it contains may * not have started yet when this method returns. * * @throws \Exception if the Docker command failed to execute. */ public function createAndStartContainer() { $moreEntropy = true; $this->containerName = uniqid($this->imageName . "-", $moreEntropy); // There is no need to start the web server as root, so it is started // directly as www-data instead. // The port 80 of the container is mapped to a free port from a range in // the host system; due to this it can be accessed from the host using // the "127.0.0.1" IP address, which prevents Nextcloud from complaining // that it is being accessed from an untrusted domain. $this->executeDockerCommand("run --detach --user=www-data --publish 127.0.0.1:" . $this->hostPortRangeForContainer . ":80 --name=" . $this->containerName . " " . $this->imageName); } /** * Stops and removes the container. * * @throws \Exception if the Docker command failed to execute. */ public function stopAndRemoveContainer() { // Although the Nextcloud image does not define a volume "--volumes" is // used anyway just in case any of its ancestor images does. $this->executeDockerCommand("rm --volumes --force " . $this->containerName); } /** * Returns the container name. * * If the container has not been created yet the container name will be * null. * * @return string the container name. */ public function getContainerName() { return $this->containerName; } /** * Returns the IP address and port of the Nextcloud test server (which is * mapped to a local port in the host). * * @return string the IP address and port as "$ipAddress:$port". * @throws \Exception if the Docker command failed to execute or the * container is not running. */ public function getNextcloudTestServerAddress() { return $this->executeDockerCommand("port " . $this->containerName . " 80"); } /** * Returns whether the container is running or not. * * @return boolean true if the container is running, false otherwise. * @throws \Exception if the Docker command failed to execute. */ public function isContainerRunning() { // By default, "docker ps" only shows running containers, and the // "--quiet" option only shows the ID of the matching containers, // without table headers. Therefore, if the container is not running the // output will be empty (not even a new line, as the last line of output // returned by "executeDockerCommand" does not include a trailing new // line character). return $this->executeDockerCommand("ps --quiet --filter 'name=" . $this->containerName . "'") !== ""; } /** * Returns whether the container exists (no matter its state) or not. * * @return boolean true if the container exists, false otherwise. * @throws \Exception if the Docker command failed to execute. */ public function isContainerRegistered() { // With the "--quiet" option "docker ps" only shows the ID of the // matching containers, without table headers. Therefore, if the // container does not exist the output will be empty (not even a new // line, as the last line of output returned by "executeDockerCommand" // does not include a trailing new line character). return $this->executeDockerCommand("ps --all --quiet --filter 'name=" . $this->containerName . "'") !== ""; } /** * Executes the given Docker command. * * @return string the last line of output, without trailing new line * character. * @throws \Exception if the Docker command failed to execute. */ private function executeDockerCommand($dockerCommand) { $output = array(); $returnValue = 0; $lastLine = exec("docker " . $dockerCommand . " 2>&1", $output, $returnValue); if ($returnValue !== 0) { throw new Exception("Failed to execute 'docker " . $dockerCommand . "': " . implode("\n", $output)); } return $lastLine; } }