cloud-8.x-2.0-beta1/modules/cloud_service_providers/docker/src/Service/DockerService.php
modules/cloud_service_providers/docker/src/Service/DockerService.php
<?php
namespace Drupal\docker\Service;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Http\ClientFactory;
use GuzzleHttp\Exception\GuzzleException;
/**
* Class DockerService.
*/
class DockerService implements DockerServiceInterface {
use StringTranslationTrait;
/**
* The docker api version to use.
*
* @var string
*/
protected $apiVersion = '';
/**
* The return format of API requests.
*
* @var string
*/
protected $format = 'json';
/**
* The docker unix socket.
*
* @var string
*/
protected $unixSocket;
/**
* Flag whether to use the unix socket in the API requests.
*
* @var bool
*/
protected $useSocket = TRUE;
/**
* The Messenger service.
*
* @var \Drupal\Core\Messenger\Messenger
*/
protected $messenger;
/**
* The config factory.
*
* Subclasses should use the self::config() method, which may be overridden to
* address specific needs when loading config, rather than this property
* directly. See \Drupal\Core\Form\ConfigFormBase::config() for an example of
* this.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Guzzle client.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* Constructs a new DockerService object.
*
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* A Logger instance.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* A configuration factory.
* @param \Drupal\Core\Messenger\Messenger $messenger
* The messenger service.
* @param \Drupal\Core\Http\ClientFactory $client_factory
* The http client.
*/
public function __construct(LoggerChannelFactoryInterface $logger_factory,
ConfigFactoryInterface $config_factory,
Messenger $messenger,
ClientFactory $client_factory
) {
$this->logger = $logger_factory->get('docker');
$this->configFactory = $config_factory;
$this->messenger = $messenger;
$this->httpClient = $client_factory->fromOptions([
// No timeout because docker images can be quite large.
'timeout' => 0,
]);
// Set the unix socket.
$this->unixSocket = $config_factory->get('docker.settings')->get('docker_unix_socket');
$this->apiVersion = $config_factory->get('docker.settings')->get('docker_api_version');
}
/**
* Helper method to do a Guzzle HTTP POST.
*
* @param string $endpoint
* The endpoint to access.
* @param array $params
* Parameters to pass to the POST request.
*
* @return string|bool
* String interpretation of the response body or FALSE
*/
protected function doPost($endpoint, array $params = []) {
$output = FALSE;
try {
$headers = array_merge_recursive($this->getDefaultParams(), $params);
$response = $this->httpClient->post(
$endpoint,
$headers
);
$output = $response->getBody()->getContents();
}
catch (GuzzleException $error) {
$response = $error->getResponse();
$response_info = $response->getBody()->getContents();
$message = new FormattableMarkup(
"Docker api error. Error details are as follows:<pre>@response</pre>",
[
'@response' => print_r(json_decode($response_info), TRUE),
]
);
$this->logError('Remote API Connection', $error, $message);
}
catch (\Exception $error) {
$this->logError('Remote API Connection', $error, $this->t('An unknown error
occurred while trying to connect to the remote API. This is not a Guzzle
error, nor an error in the remote API, rather a generic local error
occurred. The reported error was @error',
['@error' => $error->getMessage()]
));
}
return $output;
}
/**
* Helper method to do a Guzzle HTTP GET.
*
* @param string $endpoint
* The endpoint to access.
* @param array $params
* Parameters to pass to the POST request.
*
* @return string|bool
* String interpretation of the response body or FALSE
*/
protected function doGet($endpoint, array $params = []) {
$output = FALSE;
try {
$response = $this->httpClient->get(
$endpoint,
array_merge($this->getDefaultParams(), $params)
);
$output = $response->getBody()->getContents();
}
catch (GuzzleException $error) {
$response = $error->getResponse();
if ($response != NULL) {
$response_info = $response->getBody()->getContents();
$response_info = new FormattableMarkup(
"Docker api error. Error details are as follows:<pre>@response</pre>",
[
'@response' => print_r(json_decode($response_info), TRUE),
]
);
}
else {
$response_info = $error->getMessage();
}
$message = new FormattableMarkup(
"Docker api error. Error details are as follows:<pre>@response</pre>",
[
'@response' => $response_info,
]
);
$this->logError('Remote API Connection', $error, $message);
}
catch (\Exception $error) {
$this->logError('Remote API Connection', $error, $this->t('An unknown error
occurred while trying to connect to the remote API. This is not a Guzzle
error, nor an error in the remote API, rather a generic local error
occurred. The reported error was @error',
['@error' => $error->getMessage()]
));
}
return $output;
}
/**
* {@inheritdoc}
*/
public function setUnixSocket($unix_socket) {
$this->unixSocket = $unix_socket;
}
/**
* {@inheritdoc}
*/
public function setApiVersion($api_version) {
$this->apiVersion = $api_version;
}
/**
* {@inheritdoc}
*/
public function listImages() {
$output = [];
$endpoint = $this->buildEndpoint("images/$this->format");
$results = $this->doGet($endpoint);
if ($results !== FALSE) {
$output = json_decode($results);
}
return $output;
}
/**
* {@inheritdoc}
*/
public function pushImage($name, array $auth_array = []) {
$params = [];
$endpoint = $this->buildEndpoint("images/$name/push");
if (!empty($auth_array)) {
$auth_json = json_encode($auth_array);
$params['headers']['X-Registry-Auth'] = base64_encode($auth_json);
}
return $this->doPost($endpoint, $params);
}
/**
* {@inheritdoc}
*/
public function inspectImage($name) {
$endpoint = $this->buildEndpoint("images/$name/$this->format");
return $this->doGet($endpoint);
}
/**
* {@inheritdoc}
*/
public function pullImage($from_image) {
$endpoint = $this->buildEndpoint("images/create?fromImage=$from_image");
$response = $this->doPost($endpoint, []);
$success = FALSE;
// Verify image was pulled.
try {
$image = $this->inspectImage($from_image);
$success = TRUE;
}
catch (DockerServiceException $e) {
$this->logError('Docker pull image', $e, $this->t('Unable to find pulled image'), FALSE);
}
return $success;
}
/**
* {@inheritdoc}
*/
public function tagImage($name, $repo, $tag) {
$endpoint = $this->buildEndpoint("images/$name/tag?repo=$repo&tag=$tag");
$response = $this->doPost($endpoint, []);
}
/**
* {@inheritdoc}
*/
public function setFormat($format) {
$this->format = $format;
}
/**
* {@inheritdoc}
*/
public function setUseSocket($use_socket) {
$this->useSocket = $use_socket;
}
/**
* {@inheritdoc}
*/
public function parseImage($image) {
$pattern = "/^(?:([^\/]+)\/)?(?:([^\/]+)\/)?([^@:\/]+)(?:[@:](.+))?$/";
$match = preg_match($pattern, $image, $matches);
if ($match == FALSE) {
return FALSE;
}
$registry = $matches[1];
$namespace = $matches[2];
$repository = $matches[3];
$tag = isset($matches[4]) ? $matches[4] : 'latest';
// If there is no hostname, the registry variable is the namespace.
if (empty($namespace) && !empty($registry) && !preg_match('/[:.]/', $registry)) {
$namespace = $registry;
$registry = '';
}
$info = [
'namespace' => $namespace,
'registry' => $registry,
'repository' => $repository,
'tag' => $tag,
];
// Derive the namespace/repository and also the full name.
$registry = !empty($registry) ? $registry . '/' : '';
$namespace = !empty($namespace) ? $namespace . '/' : '';
$tag = ':' . $tag;
$info['full_repository'] = $namespace . $repository;
$info['name'] = $registry . $namespace . $repository . $tag;
return $info;
}
/**
* {@inheritdoc}
*/
public function isDockerUp($unix_socket = '', $api_version = '') {
$is_available = FALSE;
try {
if (!empty($unix_socket)) {
$this->setUnixSocket($unix_socket);
}
if (!empty($api_version)) {
$this->setApiVersion($api_version);
}
$this->listImages();
$is_available = TRUE;
}
catch (DockerServiceException $e) {
// Set an error if docker is unreachable.
$this->logError('Remote API Connection', $e, $this->t('Docker unreachable. @error', ['@error' => $e->getMessage()]), FALSE);
}
return $is_available;
}
/**
* Log errors to watchdog and throw an exception.
*
* @param string $type
* The error type.
* @param \Exception $exception
* The exception object.
* @param string $message
* The message to log.
* @param bool $throw
* TRUE to throw exception.
*
* @throws \Drupal\docker\Service\DockerServiceException
*/
private function logError($type, \Exception $exception, $message, $throw = TRUE) {
watchdog_exception($type, $exception, $message);
if ($throw == TRUE) {
throw new DockerServiceException($exception);
}
}
/**
* Build a local api endpoint.
*
* @param string $operation
* Docker operation.
* @param bool $https
* Build string using http or https.
*
* @return string
* Docker endpoint.
*/
private function buildEndpoint($operation, $https = FALSE) {
$endpoint = $https == TRUE ? 'https' : 'http';
$endpoint .= "://locahost";
if (!empty($this->apiVersion)) {
$endpoint .= "/v{$this->apiVersion}";
}
$endpoint .= "/$operation";
return $endpoint;
}
/**
* Setup any default parameters for the Guzzle request.
*
* @return array
* Array of parameters.
*/
private function getDefaultParams() {
$params = [
'headers' => [
'Content-Type' => 'application/json',
],
];
if ($this->useSocket == TRUE) {
$params['curl'] = [
CURLOPT_UNIX_SOCKET_PATH => $this->unixSocket,
];
}
return $params;
}
}
