<commit_before><?php

namespace UniversityOfAdelaide\OpenShift;

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\RequestException;

/**
 * Class Client.
 *
 * Provides a client using guzzle to interact easily
 * with the OpenShift api.
 *
 * @package UniversityOfAdelaide\OpenShift
 */
class Client implements ClientInterface {

  /**
   * Api version.
   *
   * @var string
   */
  private $apiVersion = 'v1';

  /**
   * Current working namespace.
   *
   * @var string
   */
  private $namespace;

  /**
   * Base url to OpenShift.
   *
   * @var string
   */
  private $host;

  /**
   * Guzzle HTTP Client.
   *
   * @var \GuzzleHttp\Client
   */
  protected $guzzleClient;

  /**
   * Resource map.
   *
   * @var array
   */
  protected $resourceMap = [
    'buildconfig' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/buildconfigs',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/oapi/v1/namespaces/{namespace}/buildconfigs/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/buildconfigs/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/oapi/v1/namespaces/{namespace}/buildconfigs/{name}',
      ],
    ],
    'builds' => [
      'get' => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/builds/{name}',
      ],
    ],
    'cronjob' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/apis/batch/v1beta1/namespaces/{namespace}/cronjobs',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/apis/batch/v1beta1/namespaces/{namespace}/cronjobs/{name}',
      ],
      'get' => [
        'action' => 'GET',
        'uri'    => '/apis/batch/v1beta1/namespaces/{namespace}/cronjobs/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/apis/batch/v1beta1/namespaces/{namespace}/cronjobs/{name}',
      ],
    ],
    'deploymentconfig' => [
      'create'      => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs',
      ],
      'delete'      => [
        'action' => 'DELETE',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs/{name}',
      ],
      'get'         => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs/{name}',
      ],
      'update'      => [
        'action' => 'PUT',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs/{name}',
      ],
      'instantiate' => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs/{name}/instantiate',
      ],
    ],
    'deploymentconfigs' => [
      'get' => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs',
      ],
    ],
    'imagestream' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreams',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreams/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreams/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreams/{name}',
      ],
    ],
    'imagestreamtag' => [
      'get' => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreamtags/{name}',
      ],
    ],
    'job' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/apis/batch/v1/namespaces/{namespace}/jobs',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/apis/batch/v1/namespaces/{namespace}/jobs/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/apis/batch/v1/namespaces/{namespace}/jobs/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/apis/batch/v1/namespaces/{namespace}/jobs/{name}',
      ],
    ],
    'persistentvolumeclaim' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/api/v1/namespaces/{namespace}/persistentvolumeclaims',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/persistentvolumeclaims/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/persistentvolumeclaims/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/api/v1/namespaces/{namespace}/persistentvolumeclaims/{name}',
      ],
    ],
    'pod' => [
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/pods/{name}',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/pods/{name}',
      ],
    ],
    'pods' => [
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/pods',
      ],
    ],
    'replicationcontrollers' => [
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/replicationcontrollers/{name}',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/replicationcontrollers/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/api/v1/namespaces/{namespace}/replicationcontrollers/{name}',
      ],
    ],
    'route' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/routes',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/oapi/v1/namespaces/{namespace}/routes/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/routes/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/oapi/v1/namespaces/{namespace}/routes/{name}',
      ],
    ],
    'secret' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/api/v1/namespaces/{namespace}/secrets',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/secrets/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/secrets/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/api/v1/namespaces/{namespace}/secrets/{name}',
      ],
    ],
    'service' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/api/v1/namespaces/{namespace}/services',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/services/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/services/{name}',
      ],
      'group'  => [
        'action' => 'PATCH',
        'uri'    => '/api/v1/namespaces/{namespace}/services/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/api/v1/namespaces/{namespace}/services/{name}',
      ],
    ],
  ];

  /**
   * Expected response codes map.
   *
   * @var array
   */
  protected $responseCodes = [
    'POST' => 201,
    'DELETE' => 200,
    'GET' => 200,
    'PUT' => 200,
    'PATCH' => 200,
  ];

  /**
   * {@inheritdoc}
   */
  public function __construct($host, $token, $namespace, $verifyTls = TRUE) {
    $this->host = $host;
    $this->namespace = $namespace;
    $this->guzzleClient = new GuzzleClient([
      'verify' => $verifyTls,
      'base_uri' => $host,
      'headers' => [
        'Authorization' => 'Bearer ' . $token,
        'Accept' => 'application/json',
      ],
    ]);
  }

  /**
   * Returns the api version.
   *
   * @return string
   *   The currently supported api version.
   */
  public function getApiVersion() {
    return $this->apiVersion;
  }

  /**
   * Set the api version number.
   *
   * @param string $apiVersion
   *   Api version number.
   */
  public function setApiVersion(string $apiVersion) {
    $this->apiVersion = $apiVersion;
  }

  /**
   * Returns the guzzle client.
   *
   * @return \GuzzleHttp\Client
   *   Return the guzzle client.
   */
  public function getGuzzleClient() {
    return $this->guzzleClient;
  }

  /**
   * {@inheritdoc}
   */
  public function request(string $method, string $uri, array $body = [], array $query = []) {
    $requestOptions = [];

    if ($method !== 'DELETE') {
      $requestOptions = [
        'query' => $query,
        'body' => json_encode($body),
      ];
    }

    if ($method === 'PATCH') {
      $requestOptions['headers']['Content-Type'] = 'application/merge-patch+json';
    }
    else {
      $requestOptions['headers']['Content-Type'] = 'application/json';
    }

    try {
      $response = $this->guzzleClient->request($method, $uri, $requestOptions);
    }
    catch (RequestException $e) {
      // If the exception is a 'not found' response to a GET, just return false.
      if ($method === 'GET' && $e->getCode() === 404) {
        return FALSE;
      }
      // Do some special decoding for OpenShift.
      if ($e->hasResponse()) {
        $message = json_decode($e->getResponse()->getBody()->getContents());
      }
      throw new ClientException(
        isset($message) ? $message->message : $e->getMessage(),
        $e->getCode(),
        $e->getPrevious(),
        $e->hasResponse() ? $e->getResponse()->getBody() : ''
      );
    }
    return json_decode($response->getBody()->getContents(), TRUE);
  }

  /**
   * Gets the uri and action from the resourceMap using the class method name.
   *
   * @param string $methodName
   *   The class method name, typically from __METHOD__ magic constant.
   *
   * @return array
   *   The information on how to call the method.
   */
  protected function getResourceMethod(string $methodName) {
    // Strip class name if present.
    $exploded_class = explode('::', $methodName);
    $methodName = end($exploded_class);
    // Split into array by snakeCaseWordBoundaries.
    $nameParts = preg_split('/(?=[A-Z])/', $methodName);
    // The first element is the action (e.g. 'create').
    $action = array_shift($nameParts);
    // The remaining elements are the resource (e.g. 'deploymentconfig').
    $resource = strtolower(implode('', $nameParts));

    return $this->resourceMap[$resource][$action];
  }

  /**
   * Creates a relative request url.
   *
   * @param string $uri
   *   The URI to be parsed.
   * @param array $params
   *   Params that map to the uri resource path e.g.
   *   /api/{namespace}/{name}.
   *
   * @return string
   *   The request uri.
   */
  protected function createRequestUri(string $uri, array $params = []) {
    // By default replace the {namespace} this is set in configuration.
    $params['namespace'] = $this->namespace;

    foreach ($params as $key => $param) {
      // Perform a string replace on the uri.
      $uri = str_replace('{' . $key . '}', $param, $uri);
    }

    return $uri;
  }

  /**
   * {@inheritdoc}
   */
  public function createSecret(string $name, array $data) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);

    // Base64 encode the data.
    foreach ($data as $key => $value) {
      $data[$key] = base64_encode($value);
    }

    // @todo - this should use model.
    $secret = [
      'api_version' => 'v1',
      'kind' => 'Secret',
      'metadata' => [
        'name' => $name,
      ],
      'type' => 'Opaque',
      'data' => $data,
    ];

    return $this->request($resourceMethod['action'], $this->createRequestUri($resourceMethod['uri']), $secret);
  }

  /**
   * {@inheritdoc}
   */
  public function getSecret(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function updateSecret(string $name, array $data) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri'], [
      'name' => $name,
    ]);

    // Base64 encode the data.
    foreach ($data as $key => $value) {
      $data[$key] = base64_encode($value);
    }

    $secret = [
      'api_version' => 'v1',
      'kind' => 'Secret',
      'metadata' => [
        'name' => $name,
      ],
      'type' => 'Opaque',
      'data' => $data,
    ];

    return $this->request($resourceMethod['action'], $uri, $secret);
  }

  /**
   * {@inheritdoc}
   */
  public function deleteSecret(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function getService(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function createService(string $name, string $deployment_name, int $port, int $target_port, string $app_name) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri']);

    // @todo - use a model.
    $service = [
      'kind' => 'Service',
      'metadata' => [
        'name' => $name,
        'labels' => ['app' => $app_name],
      ],
      'spec' => [
        'ports' => [
          // Defaults to TCP.
          [
            // @todo Does this have any impact when using non web ports?
            'name' => 'web',
            'port' => $port,
            'targetPort' => $target_port,
          ],
        ],
        'selector' => [
          'deploymentconfig' => $deployment_name,
        ],
      ],
    ];

    $annotations = ['app' => $app_name];
    $this->applyAnnotations($service, $annotations);

    $result = $this->request($resourceMethod['action'], $uri, $service);
    if ($result && $app_name !== $name) {
      // @todo - there is a possibility for a race condition if the group triggers
      // before the previous request has been completed.
      $this->groupService($app_name, $name);
    }

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function updateService(string $name, string $selector) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri'], [
      'name' => $name,
    ]);

    $service = $this->getService($name);

    $service['spec']['selector']['deploymentconfig'] = $selector;

    return $this->request($resourceMethod['action'], $uri, $service);
  }

  /**
   * {@inheritdoc}
   */
  public function groupService(string $app_name, string $name) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri'], [
      'name' => $app_name,
    ]);

    $parent_service = $this->getService($app_name);
    if ($parent_service) {
      $annotations['service.alpha.openshift.io/dependencies'] =
        t('[{"name": "@name", "kind": "Service"}]', ['@name' => $name]);

      $this->request($resourceMethod['action'], $uri, ['metadata' => ['annotations' => $annotations]]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function deleteService(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function getRoute(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function createRoute(string $name, string $service_name, string $domain, string $path = NULL, array $annotations = []) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri']);

    $route = [
      'kind' => 'Route',
      'metadata' => [
        'name' => $name,
        'labels' => ['app' => $name],
      ],
      'spec' => [
        'host' => $domain,
        'path' => $path,
        'to' => [
          'kind' => 'Service',
          'name' => $service_name,
        ],
      ],
    ];

    if (count($annotations)) {
      $route['metadata']['annotations'] = $annotations;
    }

    return $this->request($resourceMethod['action'], $uri, $route);
  }

  /**
   * {@inheritdoc}
   */
  public function updateRoute(string $name, string $service_name, string $application_domain) {
    // TODO: Implement updateRoute() method.
  }

  /**
   * {@inheritdoc}
   */
  public function deleteRoute(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function getBuilds(string $name, string $label) {
    return $this->apiCall(__METHOD__, '', $label);
  }

  /**
   * {@inheritdoc}
   */
  public function getBuildConfig(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function generateBuildConfig(string $name, string $secret, string $image_stream_tag, array $data) {
    $build_config = [
      'kind' => 'BuildConfig',
      'metadata' => [
        'annotations' => [
          'description' => 'Defines how to build the application',
        ],
        'name' => $name,
      ],
      'spec' => [
        'resources' => [
          'requests' => [
            'cpu' => $data['cpu_request'] ?? '0m',
            'memory' => $data['memory_request'] ?? '0Mi',
          ],
        ],
        'output' => [
          'to' => [
            'kind' => 'ImageStreamTag',
            'name' => $image_stream_tag,
          ],
        ],
        'source' => [<commit_msg>Add in the limits as well, while we are here.<commit_after><?php

namespace UniversityOfAdelaide\OpenShift;

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\RequestException;

/**
 * Class Client.
 *
 * Provides a client using guzzle to interact easily
 * with the OpenShift api.
 *
 * @package UniversityOfAdelaide\OpenShift
 */
class Client implements ClientInterface {

  /**
   * Api version.
   *
   * @var string
   */
  private $apiVersion = 'v1';

  /**
   * Current working namespace.
   *
   * @var string
   */
  private $namespace;

  /**
   * Base url to OpenShift.
   *
   * @var string
   */
  private $host;

  /**
   * Guzzle HTTP Client.
   *
   * @var \GuzzleHttp\Client
   */
  protected $guzzleClient;

  /**
   * Resource map.
   *
   * @var array
   */
  protected $resourceMap = [
    'buildconfig' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/buildconfigs',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/oapi/v1/namespaces/{namespace}/buildconfigs/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/buildconfigs/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/oapi/v1/namespaces/{namespace}/buildconfigs/{name}',
      ],
    ],
    'builds' => [
      'get' => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/builds/{name}',
      ],
    ],
    'cronjob' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/apis/batch/v1beta1/namespaces/{namespace}/cronjobs',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/apis/batch/v1beta1/namespaces/{namespace}/cronjobs/{name}',
      ],
      'get' => [
        'action' => 'GET',
        'uri'    => '/apis/batch/v1beta1/namespaces/{namespace}/cronjobs/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/apis/batch/v1beta1/namespaces/{namespace}/cronjobs/{name}',
      ],
    ],
    'deploymentconfig' => [
      'create'      => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs',
      ],
      'delete'      => [
        'action' => 'DELETE',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs/{name}',
      ],
      'get'         => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs/{name}',
      ],
      'update'      => [
        'action' => 'PUT',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs/{name}',
      ],
      'instantiate' => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs/{name}/instantiate',
      ],
    ],
    'deploymentconfigs' => [
      'get' => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/deploymentconfigs',
      ],
    ],
    'imagestream' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreams',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreams/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreams/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreams/{name}',
      ],
    ],
    'imagestreamtag' => [
      'get' => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/imagestreamtags/{name}',
      ],
    ],
    'job' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/apis/batch/v1/namespaces/{namespace}/jobs',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/apis/batch/v1/namespaces/{namespace}/jobs/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/apis/batch/v1/namespaces/{namespace}/jobs/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/apis/batch/v1/namespaces/{namespace}/jobs/{name}',
      ],
    ],
    'persistentvolumeclaim' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/api/v1/namespaces/{namespace}/persistentvolumeclaims',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/persistentvolumeclaims/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/persistentvolumeclaims/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/api/v1/namespaces/{namespace}/persistentvolumeclaims/{name}',
      ],
    ],
    'pod' => [
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/pods/{name}',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/pods/{name}',
      ],
    ],
    'pods' => [
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/pods',
      ],
    ],
    'replicationcontrollers' => [
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/replicationcontrollers/{name}',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/replicationcontrollers/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/api/v1/namespaces/{namespace}/replicationcontrollers/{name}',
      ],
    ],
    'route' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/oapi/v1/namespaces/{namespace}/routes',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/oapi/v1/namespaces/{namespace}/routes/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/oapi/v1/namespaces/{namespace}/routes/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/oapi/v1/namespaces/{namespace}/routes/{name}',
      ],
    ],
    'secret' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/api/v1/namespaces/{namespace}/secrets',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/secrets/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/secrets/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/api/v1/namespaces/{namespace}/secrets/{name}',
      ],
    ],
    'service' => [
      'create' => [
        'action' => 'POST',
        'uri'    => '/api/v1/namespaces/{namespace}/services',
      ],
      'delete' => [
        'action' => 'DELETE',
        'uri'    => '/api/v1/namespaces/{namespace}/services/{name}',
      ],
      'get'    => [
        'action' => 'GET',
        'uri'    => '/api/v1/namespaces/{namespace}/services/{name}',
      ],
      'group'  => [
        'action' => 'PATCH',
        'uri'    => '/api/v1/namespaces/{namespace}/services/{name}',
      ],
      'update' => [
        'action' => 'PUT',
        'uri'    => '/api/v1/namespaces/{namespace}/services/{name}',
      ],
    ],
  ];

  /**
   * Expected response codes map.
   *
   * @var array
   */
  protected $responseCodes = [
    'POST' => 201,
    'DELETE' => 200,
    'GET' => 200,
    'PUT' => 200,
    'PATCH' => 200,
  ];

  /**
   * {@inheritdoc}
   */
  public function __construct($host, $token, $namespace, $verifyTls = TRUE) {
    $this->host = $host;
    $this->namespace = $namespace;
    $this->guzzleClient = new GuzzleClient([
      'verify' => $verifyTls,
      'base_uri' => $host,
      'headers' => [
        'Authorization' => 'Bearer ' . $token,
        'Accept' => 'application/json',
      ],
    ]);
  }

  /**
   * Returns the api version.
   *
   * @return string
   *   The currently supported api version.
   */
  public function getApiVersion() {
    return $this->apiVersion;
  }

  /**
   * Set the api version number.
   *
   * @param string $apiVersion
   *   Api version number.
   */
  public function setApiVersion(string $apiVersion) {
    $this->apiVersion = $apiVersion;
  }

  /**
   * Returns the guzzle client.
   *
   * @return \GuzzleHttp\Client
   *   Return the guzzle client.
   */
  public function getGuzzleClient() {
    return $this->guzzleClient;
  }

  /**
   * {@inheritdoc}
   */
  public function request(string $method, string $uri, array $body = [], array $query = []) {
    $requestOptions = [];

    if ($method !== 'DELETE') {
      $requestOptions = [
        'query' => $query,
        'body' => json_encode($body),
      ];
    }

    if ($method === 'PATCH') {
      $requestOptions['headers']['Content-Type'] = 'application/merge-patch+json';
    }
    else {
      $requestOptions['headers']['Content-Type'] = 'application/json';
    }

    try {
      $response = $this->guzzleClient->request($method, $uri, $requestOptions);
    }
    catch (RequestException $e) {
      // If the exception is a 'not found' response to a GET, just return false.
      if ($method === 'GET' && $e->getCode() === 404) {
        return FALSE;
      }
      // Do some special decoding for OpenShift.
      if ($e->hasResponse()) {
        $message = json_decode($e->getResponse()->getBody()->getContents());
      }
      throw new ClientException(
        isset($message) ? $message->message : $e->getMessage(),
        $e->getCode(),
        $e->getPrevious(),
        $e->hasResponse() ? $e->getResponse()->getBody() : ''
      );
    }
    return json_decode($response->getBody()->getContents(), TRUE);
  }

  /**
   * Gets the uri and action from the resourceMap using the class method name.
   *
   * @param string $methodName
   *   The class method name, typically from __METHOD__ magic constant.
   *
   * @return array
   *   The information on how to call the method.
   */
  protected function getResourceMethod(string $methodName) {
    // Strip class name if present.
    $exploded_class = explode('::', $methodName);
    $methodName = end($exploded_class);
    // Split into array by snakeCaseWordBoundaries.
    $nameParts = preg_split('/(?=[A-Z])/', $methodName);
    // The first element is the action (e.g. 'create').
    $action = array_shift($nameParts);
    // The remaining elements are the resource (e.g. 'deploymentconfig').
    $resource = strtolower(implode('', $nameParts));

    return $this->resourceMap[$resource][$action];
  }

  /**
   * Creates a relative request url.
   *
   * @param string $uri
   *   The URI to be parsed.
   * @param array $params
   *   Params that map to the uri resource path e.g.
   *   /api/{namespace}/{name}.
   *
   * @return string
   *   The request uri.
   */
  protected function createRequestUri(string $uri, array $params = []) {
    // By default replace the {namespace} this is set in configuration.
    $params['namespace'] = $this->namespace;

    foreach ($params as $key => $param) {
      // Perform a string replace on the uri.
      $uri = str_replace('{' . $key . '}', $param, $uri);
    }

    return $uri;
  }

  /**
   * {@inheritdoc}
   */
  public function createSecret(string $name, array $data) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);

    // Base64 encode the data.
    foreach ($data as $key => $value) {
      $data[$key] = base64_encode($value);
    }

    // @todo - this should use model.
    $secret = [
      'api_version' => 'v1',
      'kind' => 'Secret',
      'metadata' => [
        'name' => $name,
      ],
      'type' => 'Opaque',
      'data' => $data,
    ];

    return $this->request($resourceMethod['action'], $this->createRequestUri($resourceMethod['uri']), $secret);
  }

  /**
   * {@inheritdoc}
   */
  public function getSecret(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function updateSecret(string $name, array $data) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri'], [
      'name' => $name,
    ]);

    // Base64 encode the data.
    foreach ($data as $key => $value) {
      $data[$key] = base64_encode($value);
    }

    $secret = [
      'api_version' => 'v1',
      'kind' => 'Secret',
      'metadata' => [
        'name' => $name,
      ],
      'type' => 'Opaque',
      'data' => $data,
    ];

    return $this->request($resourceMethod['action'], $uri, $secret);
  }

  /**
   * {@inheritdoc}
   */
  public function deleteSecret(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function getService(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function createService(string $name, string $deployment_name, int $port, int $target_port, string $app_name) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri']);

    // @todo - use a model.
    $service = [
      'kind' => 'Service',
      'metadata' => [
        'name' => $name,
        'labels' => ['app' => $app_name],
      ],
      'spec' => [
        'ports' => [
          // Defaults to TCP.
          [
            // @todo Does this have any impact when using non web ports?
            'name' => 'web',
            'port' => $port,
            'targetPort' => $target_port,
          ],
        ],
        'selector' => [
          'deploymentconfig' => $deployment_name,
        ],
      ],
    ];

    $annotations = ['app' => $app_name];
    $this->applyAnnotations($service, $annotations);

    $result = $this->request($resourceMethod['action'], $uri, $service);
    if ($result && $app_name !== $name) {
      // @todo - there is a possibility for a race condition if the group triggers
      // before the previous request has been completed.
      $this->groupService($app_name, $name);
    }

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function updateService(string $name, string $selector) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri'], [
      'name' => $name,
    ]);

    $service = $this->getService($name);

    $service['spec']['selector']['deploymentconfig'] = $selector;

    return $this->request($resourceMethod['action'], $uri, $service);
  }

  /**
   * {@inheritdoc}
   */
  public function groupService(string $app_name, string $name) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri'], [
      'name' => $app_name,
    ]);

    $parent_service = $this->getService($app_name);
    if ($parent_service) {
      $annotations['service.alpha.openshift.io/dependencies'] =
        t('[{"name": "@name", "kind": "Service"}]', ['@name' => $name]);

      $this->request($resourceMethod['action'], $uri, ['metadata' => ['annotations' => $annotations]]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function deleteService(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function getRoute(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function createRoute(string $name, string $service_name, string $domain, string $path = NULL, array $annotations = []) {
    $resourceMethod = $this->getResourceMethod(__METHOD__);
    $uri = $this->createRequestUri($resourceMethod['uri']);

    $route = [
      'kind' => 'Route',
      'metadata' => [
        'name' => $name,
        'labels' => ['app' => $name],
      ],
      'spec' => [
        'host' => $domain,
        'path' => $path,
        'to' => [
          'kind' => 'Service',
          'name' => $service_name,
        ],
      ],
    ];

    if (count($annotations)) {
      $route['metadata']['annotations'] = $annotations;
    }

    return $this->request($resourceMethod['action'], $uri, $route);
  }

  /**
   * {@inheritdoc}
   */
  public function updateRoute(string $name, string $service_name, string $application_domain) {
    // TODO: Implement updateRoute() method.
  }

  /**
   * {@inheritdoc}
   */
  public function deleteRoute(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function getBuilds(string $name, string $label) {
    return $this->apiCall(__METHOD__, '', $label);
  }

  /**
   * {@inheritdoc}
   */
  public function getBuildConfig(string $name) {
    return $this->apiCall(__METHOD__, $name);
  }

  /**
   * {@inheritdoc}
   */
  public function generateBuildConfig(string $name, string $secret, string $image_stream_tag, array $data) {
    $build_config = [
      'kind' => 'BuildConfig',
      'metadata' => [
        'annotations' => [
          'description' => 'Defines how to build the application',
        ],
        'name' => $name,
      ],
      'spec' => [
        'resources' => [
          'limits' => [
            'cpu' => $data['cpu_limit'] ?? '0m',
            'memory' => $data['memory_limit'] ?? '0Mi',
          ],
          'requests' => [
            'cpu' => $data['cpu_request'] ?? '0m',
            'memory' => $data['memory_request'] ?? '0Mi',
          ],
        ],
        'output' => [
          'to' => [
            'kind' => 'ImageStreamTag',
            'name' => $image_stream_tag,
          ],
        ],
        'source' => [
{'fraction_non_alphanumeric': 0.19552993297739407, 'fraction_numerical': 0.004714301942519596, 'mean_word_length': 2.1364567560345598, 'pattern_counts': {'":': 4, '<': 5, '<?xml version=': 0, '>': 570, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '712203', 'n_tokens_mistral': 11488, 'n_tokens_neox': 11384, 'n_words': 2058}