372 lines
11 KiB
PHP
372 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Dokploy;
|
|
|
|
use App\Models\InfrastructureActionLog;
|
|
use Illuminate\Contracts\Auth\Authenticatable;
|
|
use Illuminate\Http\Client\Factory as HttpFactory;
|
|
use Illuminate\Http\Client\PendingRequest;
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Http\Client\Response;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class DokployClient
|
|
{
|
|
public function __construct(private readonly HttpFactory $http) {}
|
|
|
|
public function applicationStatus(string $applicationId): array
|
|
{
|
|
return $this->cached($this->applicationCacheKey($applicationId), function () use ($applicationId) {
|
|
$application = $this->get('/application.one', [
|
|
'applicationId' => $applicationId,
|
|
]);
|
|
|
|
$appName = Arr::get($application, 'appName') ?? Arr::get($application, 'name');
|
|
$monitoring = [];
|
|
|
|
if ($appName) {
|
|
$monitoring = $this->optionalGet('/application.readAppMonitoring', [
|
|
'appName' => $appName,
|
|
]);
|
|
}
|
|
|
|
return [
|
|
'application' => $application,
|
|
'monitoring' => $monitoring,
|
|
'appName' => $appName,
|
|
];
|
|
}, 30);
|
|
}
|
|
|
|
public function projects(): array
|
|
{
|
|
return $this->cached($this->projectsCacheKey(), function () {
|
|
$projects = $this->get('/project.all');
|
|
|
|
return is_array($projects) ? $projects : [];
|
|
}, 60);
|
|
}
|
|
|
|
public function findProject(string $projectIdOrName): ?array
|
|
{
|
|
$projects = $this->projects();
|
|
|
|
foreach ($projects as $project) {
|
|
if (Arr::get($project, 'projectId') === $projectIdOrName) {
|
|
return $project;
|
|
}
|
|
}
|
|
|
|
foreach ($projects as $project) {
|
|
if (
|
|
Arr::get($project, 'name') === $projectIdOrName
|
|
|| Arr::get($project, 'projectName') === $projectIdOrName
|
|
) {
|
|
return $project;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function recentDeployments(string $applicationId, int $limit = 5): array
|
|
{
|
|
return $this->cached($this->deploymentCacheKey($applicationId), function () use ($applicationId, $limit) {
|
|
$deployments = $this->get('/deployment.all', [
|
|
'applicationId' => $applicationId,
|
|
]);
|
|
|
|
if (! is_array($deployments)) {
|
|
return [];
|
|
}
|
|
|
|
return array_slice($deployments, 0, $limit);
|
|
}, 60);
|
|
}
|
|
|
|
public function reloadApplication(string $applicationId, ?Authenticatable $actor = null): array
|
|
{
|
|
$status = $this->applicationStatus($applicationId);
|
|
$appName = Arr::get($status, 'appName');
|
|
|
|
if (! $appName) {
|
|
throw new \RuntimeException('Dokploy application name is required to reload the service.');
|
|
}
|
|
|
|
$response = $this->dispatchAction(
|
|
$applicationId,
|
|
'reload',
|
|
[
|
|
'applicationId' => $applicationId,
|
|
'appName' => $appName,
|
|
],
|
|
fn (array $payload) => $this->post('/application.reload', $payload),
|
|
$actor,
|
|
);
|
|
|
|
$this->forgetApplicationCaches($applicationId);
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function redeployApplication(string $applicationId, ?Authenticatable $actor = null): array
|
|
{
|
|
$response = $this->dispatchAction(
|
|
$applicationId,
|
|
'redeploy',
|
|
[
|
|
'applicationId' => $applicationId,
|
|
],
|
|
fn (array $payload) => $this->post('/application.redeploy', $payload),
|
|
$actor,
|
|
);
|
|
|
|
$this->forgetApplicationCaches($applicationId);
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function composeStatus(string $composeId): array
|
|
{
|
|
return $this->cached($this->composeCacheKey($composeId), function () use ($composeId) {
|
|
$compose = $this->get('/compose.one', [
|
|
'composeId' => $composeId,
|
|
]);
|
|
|
|
$services = $this->optionalGet('/compose.loadServices', [
|
|
'composeId' => $composeId,
|
|
'type' => 'cache',
|
|
]);
|
|
|
|
return [
|
|
'compose' => $compose,
|
|
'services' => $services,
|
|
];
|
|
}, 30);
|
|
}
|
|
|
|
public function composeDeployments(string $composeId, int $limit = 5): array
|
|
{
|
|
return $this->cached($this->composeDeploymentsCacheKey($composeId), function () use ($composeId, $limit) {
|
|
$deployments = $this->get('/deployment.allByCompose', [
|
|
'composeId' => $composeId,
|
|
]);
|
|
|
|
if (! is_array($deployments)) {
|
|
return [];
|
|
}
|
|
|
|
return array_slice($deployments, 0, $limit);
|
|
}, 60);
|
|
}
|
|
|
|
public function redeployCompose(string $composeId, ?Authenticatable $actor = null): array
|
|
{
|
|
$response = $this->dispatchAction(
|
|
$composeId,
|
|
'compose.redeploy',
|
|
[
|
|
'composeId' => $composeId,
|
|
],
|
|
fn (array $payload) => $this->post('/compose.redeploy', $payload),
|
|
$actor,
|
|
);
|
|
|
|
$this->forgetComposeCaches($composeId);
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function deployCompose(string $composeId, ?Authenticatable $actor = null): array
|
|
{
|
|
$response = $this->dispatchAction(
|
|
$composeId,
|
|
'compose.deploy',
|
|
[
|
|
'composeId' => $composeId,
|
|
],
|
|
fn (array $payload) => $this->post('/compose.deploy', $payload),
|
|
$actor,
|
|
);
|
|
|
|
$this->forgetComposeCaches($composeId);
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function stopCompose(string $composeId, ?Authenticatable $actor = null): array
|
|
{
|
|
$response = $this->dispatchAction(
|
|
$composeId,
|
|
'compose.stop',
|
|
[
|
|
'composeId' => $composeId,
|
|
],
|
|
fn (array $payload) => $this->post('/compose.stop', $payload),
|
|
$actor,
|
|
);
|
|
|
|
$this->forgetComposeCaches($composeId);
|
|
|
|
return $response;
|
|
}
|
|
|
|
protected function cached(string $key, callable $callback, int $seconds): mixed
|
|
{
|
|
return Cache::remember($key, now()->addSeconds($seconds), $callback);
|
|
}
|
|
|
|
protected function optionalGet(string $path, array $query = []): array
|
|
{
|
|
try {
|
|
return $this->get($path, $query);
|
|
} catch (\Throwable $exception) {
|
|
Log::warning('[Dokploy] Optional GET failed', [
|
|
'path' => $path,
|
|
'message' => $exception->getMessage(),
|
|
]);
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
protected function get(string $path, array $query = []): array
|
|
{
|
|
$response = $this->request()->get($this->normalizePath($path), $query);
|
|
|
|
if ($response->failed()) {
|
|
$this->logFailure('GET', $path, $response);
|
|
throw new RequestException($response);
|
|
}
|
|
|
|
return $response->json() ?? [];
|
|
}
|
|
|
|
protected function post(string $path, array $payload = []): Response
|
|
{
|
|
$response = $this->request()->post($this->normalizePath($path), $payload);
|
|
|
|
if ($response->failed()) {
|
|
$this->logFailure('POST', $path, $response);
|
|
throw new RequestException($response);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
protected function request(): PendingRequest
|
|
{
|
|
$baseUrl = config('dokploy.api.base_url');
|
|
$token = config('dokploy.api.token');
|
|
$timeout = config('dokploy.api.timeout', 10);
|
|
|
|
if (! $baseUrl || ! $token) {
|
|
throw new \RuntimeException('Dokploy API is not configured.');
|
|
}
|
|
|
|
return $this->http
|
|
->baseUrl($baseUrl)
|
|
->timeout($timeout)
|
|
->acceptJson()
|
|
->withHeaders([
|
|
'x-api-key' => $token,
|
|
]);
|
|
}
|
|
|
|
protected function normalizePath(string $path): string
|
|
{
|
|
return '/'.ltrim($path, '/');
|
|
}
|
|
|
|
protected function logFailure(string $method, string $path, Response $response): void
|
|
{
|
|
Log::error('[Dokploy] API request failed', [
|
|
'method' => $method,
|
|
'path' => $path,
|
|
'status' => $response->status(),
|
|
'body' => $response->body(),
|
|
]);
|
|
}
|
|
|
|
protected function dispatchAction(
|
|
string $targetId,
|
|
string $action,
|
|
array $payload,
|
|
callable $callback,
|
|
?Authenticatable $actor = null,
|
|
): array {
|
|
try {
|
|
$response = $callback($payload);
|
|
$body = $response->json() ?? [];
|
|
$status = $response->status();
|
|
} catch (\Throwable $exception) {
|
|
$this->logAction($targetId, $action, $payload, [
|
|
'error' => $exception->getMessage(),
|
|
], null, $actor);
|
|
|
|
throw $exception;
|
|
}
|
|
|
|
$this->logAction($targetId, $action, $payload, $body, $status, $actor);
|
|
|
|
return $body;
|
|
}
|
|
|
|
protected function logAction(
|
|
string $targetId,
|
|
string $action,
|
|
array $payload,
|
|
array $response,
|
|
?int $status,
|
|
?Authenticatable $actor = null,
|
|
): void {
|
|
InfrastructureActionLog::create([
|
|
'user_id' => $actor?->getAuthIdentifier() ?? auth()->id(),
|
|
'service_id' => $targetId,
|
|
'action' => $action,
|
|
'payload' => $payload,
|
|
'response' => $response,
|
|
'status_code' => $status,
|
|
]);
|
|
}
|
|
|
|
protected function applicationCacheKey(string $applicationId): string
|
|
{
|
|
return "dokploy.application.{$applicationId}";
|
|
}
|
|
|
|
protected function deploymentCacheKey(string $applicationId): string
|
|
{
|
|
return "dokploy.deployments.{$applicationId}";
|
|
}
|
|
|
|
protected function composeCacheKey(string $composeId): string
|
|
{
|
|
return "dokploy.compose.{$composeId}";
|
|
}
|
|
|
|
protected function composeDeploymentsCacheKey(string $composeId): string
|
|
{
|
|
return "dokploy.compose.deployments.{$composeId}";
|
|
}
|
|
|
|
protected function projectsCacheKey(): string
|
|
{
|
|
return 'dokploy.projects';
|
|
}
|
|
|
|
protected function forgetApplicationCaches(string $applicationId): void
|
|
{
|
|
Cache::forget($this->applicationCacheKey($applicationId));
|
|
Cache::forget($this->deploymentCacheKey($applicationId));
|
|
}
|
|
|
|
protected function forgetComposeCaches(string $composeId): void
|
|
{
|
|
Cache::forget($this->composeCacheKey($composeId));
|
|
Cache::forget($this->composeDeploymentsCacheKey($composeId));
|
|
}
|
|
}
|