124 lines
4.2 KiB
PHP
124 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
use RuntimeException;
|
|
|
|
class hetzner_cloud_api_exception extends RuntimeException
|
|
{
|
|
public function __construct(
|
|
string $message,
|
|
private readonly int $statusCode = 0,
|
|
private readonly string $apiCode = ''
|
|
) {
|
|
parent::__construct($message, $statusCode);
|
|
}
|
|
|
|
public function statusCode(): int
|
|
{
|
|
return $this->statusCode;
|
|
}
|
|
|
|
public function apiCode(): string
|
|
{
|
|
return $this->apiCode;
|
|
}
|
|
}
|
|
|
|
class hetzner_cloud_client
|
|
{
|
|
private const BASE_URL = 'https://api.hetzner.cloud/v1';
|
|
|
|
public function __construct(private readonly string $token, private readonly int $timeoutSeconds = 8)
|
|
{
|
|
if (trim($token) === '') {
|
|
throw new RuntimeException('Hetzner Cloud API token is required.');
|
|
}
|
|
}
|
|
|
|
public function getLoadBalancer(int|string $id): array
|
|
{
|
|
return $this->request('GET', '/load_balancers/' . rawurlencode((string)$id))['load_balancer'] ?? [];
|
|
}
|
|
|
|
public function addIpTarget(int|string $loadBalancerId, string $ip): array
|
|
{
|
|
return $this->request('POST', '/load_balancers/' . rawurlencode((string)$loadBalancerId) . '/actions/add_target', [
|
|
'type' => 'ip',
|
|
'ip' => ['ip' => $ip],
|
|
]);
|
|
}
|
|
|
|
public function removeIpTarget(int|string $loadBalancerId, string $ip): array
|
|
{
|
|
return $this->request('POST', '/load_balancers/' . rawurlencode((string)$loadBalancerId) . '/actions/remove_target', [
|
|
'type' => 'ip',
|
|
'ip' => ['ip' => $ip],
|
|
]);
|
|
}
|
|
|
|
public function addService(int|string $loadBalancerId, string $protocol, int $listenPort, int $destinationPort): array
|
|
{
|
|
return $this->request('POST', '/load_balancers/' . rawurlencode((string)$loadBalancerId) . '/actions/add_service', [
|
|
'protocol' => strtolower($protocol),
|
|
'listen_port' => $listenPort,
|
|
'destination_port' => $destinationPort,
|
|
'proxyprotocol' => false,
|
|
]);
|
|
}
|
|
|
|
private function request(string $method, string $path, ?array $payload = null): array
|
|
{
|
|
$curl = curl_init(self::BASE_URL . $path);
|
|
if ($curl === false) {
|
|
throw new RuntimeException('Could not initialize Hetzner Cloud API request.');
|
|
}
|
|
|
|
$headers = [
|
|
'Accept: application/json',
|
|
'Authorization: Bearer ' . trim($this->token),
|
|
];
|
|
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, strtoupper($method));
|
|
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, min(3, $this->timeoutSeconds));
|
|
curl_setopt($curl, CURLOPT_TIMEOUT, max(1, $this->timeoutSeconds));
|
|
curl_setopt($curl, CURLOPT_NOSIGNAL, true);
|
|
curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
|
|
|
|
if ($payload !== null) {
|
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
if ($body === false) {
|
|
throw new RuntimeException('Could not encode Hetzner Cloud API payload.');
|
|
}
|
|
$headers[] = 'Content-Type: application/json';
|
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
|
|
}
|
|
|
|
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
|
|
|
$raw = curl_exec($curl);
|
|
$error = curl_error($curl);
|
|
$status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|
curl_close($curl);
|
|
|
|
if ($raw === false) {
|
|
throw new RuntimeException('Hetzner Cloud API request failed: ' . $error);
|
|
}
|
|
|
|
$decoded = trim((string)$raw) === '' ? [] : json_decode((string)$raw, true);
|
|
if (!is_array($decoded)) {
|
|
$decoded = ['raw' => (string)$raw];
|
|
}
|
|
|
|
if ($status < 200 || $status >= 300) {
|
|
$errorPayload = is_array($decoded['error'] ?? null) ? $decoded['error'] : [];
|
|
$apiCode = (string)($errorPayload['code'] ?? $decoded['code'] ?? '');
|
|
$message = (string)($errorPayload['message'] ?? $decoded['message'] ?? ('HTTP ' . $status));
|
|
throw new hetzner_cloud_api_exception('Hetzner Cloud API request failed: ' . $message, $status, $apiCode);
|
|
}
|
|
|
|
return $decoded;
|
|
}
|
|
}
|