mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-25 17:01:49 -04:00
feat(api): expose runtime logs for preview deployments (#11884)
This commit is contained in:
@@ -2455,6 +2455,59 @@ class ApplicationsController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[OA\Get(
|
||||
summary: 'Get preview application logs.',
|
||||
description: 'Get runtime container logs for a preview deployment by application UUID and pull request ID.',
|
||||
path: '/applications/{uuid}/previews/{pull_request_id}/logs',
|
||||
operationId: 'get-preview-application-logs-by-pull-request-id',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string'),
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'pull_request_id',
|
||||
in: 'path',
|
||||
description: 'Pull request ID of the preview deployment.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'integer', minimum: 1),
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'lines',
|
||||
in: 'query',
|
||||
description: 'Number of lines to show from the end of the logs. Use `all` to return all logs. `-1` remains available as a compatibility alias.',
|
||||
required: false,
|
||||
schema: new OA\Schema(oneOf: [
|
||||
new OA\Schema(type: 'integer', format: 'int32', default: 100, minimum: -1, maximum: 10000),
|
||||
new OA\Schema(type: 'string', enum: ['all']),
|
||||
])
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'show_timestamps',
|
||||
in: 'query',
|
||||
description: 'Show timestamps in the logs.',
|
||||
required: false,
|
||||
schema: new OA\Schema(type: 'boolean', default: false),
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Preview runtime logs.', content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [new OA\Property(property: 'logs', type: 'string')],
|
||||
)),
|
||||
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
],
|
||||
)]
|
||||
public function logs_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
@@ -2470,7 +2523,25 @@ class ApplicationsController extends Controller
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id);
|
||||
$this->authorize('view', $application);
|
||||
|
||||
$pullRequestId = null;
|
||||
$pullRequestIdRaw = $request->route('pull_request_id');
|
||||
if ($pullRequestIdRaw !== null) {
|
||||
if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0) {
|
||||
return response()->json(['message' => 'Invalid pull_request_id.'], 422);
|
||||
}
|
||||
$pullRequestId = (int) $pullRequestIdRaw;
|
||||
|
||||
$previewExists = ApplicationPreview::where('application_id', $application->id)
|
||||
->where('pull_request_id', $pullRequestId)
|
||||
->exists();
|
||||
if (! $previewExists) {
|
||||
return response()->json(['message' => 'Preview not found.'], 404);
|
||||
}
|
||||
}
|
||||
|
||||
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pullRequestId);
|
||||
|
||||
if ($containers->count() == 0) {
|
||||
return response()->json([
|
||||
|
||||
@@ -3508,6 +3508,104 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/previews\/{pull_request_id}\/logs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Get preview application logs.",
|
||||
"description": "Get runtime container logs for a preview deployment by application UUID and pull request ID.",
|
||||
"operationId": "get-preview-application-logs-by-pull-request-id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pull_request_id",
|
||||
"in": "path",
|
||||
"description": "Pull request ID of the preview deployment.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lines",
|
||||
"in": "query",
|
||||
"description": "Number of lines to show from the end of the logs. Use `all` to return all logs. `-1` remains available as a compatibility alias.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"default": 100,
|
||||
"maximum": 10000,
|
||||
"minimum": -1
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"all"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "show_timestamps",
|
||||
"in": "query",
|
||||
"description": "Show timestamps in the logs.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Preview runtime logs.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"logs": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/envs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
@@ -2307,6 +2307,74 @@ paths:
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/previews/{pull_request_id}/logs':
|
||||
get:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Get preview application logs.'
|
||||
description: 'Get runtime container logs for a preview deployment by application UUID and pull request ID.'
|
||||
operationId: get-preview-application-logs-by-pull-request-id
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: pull_request_id
|
||||
in: path
|
||||
description: 'Pull request ID of the preview deployment.'
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
-
|
||||
name: lines
|
||||
in: query
|
||||
description: 'Number of lines to show from the end of the logs. Use `all` to return all logs. `-1` remains available as a compatibility alias.'
|
||||
required: false
|
||||
schema:
|
||||
oneOf:
|
||||
-
|
||||
type: integer
|
||||
format: int32
|
||||
default: 100
|
||||
maximum: 10000
|
||||
minimum: -1
|
||||
-
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
-
|
||||
name: show_timestamps
|
||||
in: query
|
||||
description: 'Show timestamps in the logs.'
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
'200':
|
||||
description: 'Preview runtime logs.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
logs: { type: string }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/envs':
|
||||
get:
|
||||
tags:
|
||||
|
||||
@@ -244,6 +244,7 @@ Route::group([
|
||||
Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
|
||||
Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
||||
Route::get('/applications/{uuid}/logs', [ApplicationsController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
|
||||
Route::get('/applications/{uuid}/previews/{pull_request_id}/logs', [ApplicationsController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
|
||||
Route::get('/applications/{uuid}/storages', [ApplicationsController::class, 'storages'])->middleware(['api.ability:read']);
|
||||
Route::post('/applications/{uuid}/storages', [ApplicationsController::class, 'create_storage'])->middleware(['api.ability:write']);
|
||||
Route::patch('/applications/{uuid}/storages', [ApplicationsController::class, 'update_storage'])->middleware(['api.ability:write']);
|
||||
|
||||
@@ -5,6 +5,7 @@ use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
@@ -13,6 +14,7 @@ use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -133,6 +135,61 @@ describe('DELETE /api/v1/applications/{uuid}/previews/{pull_request_id}', functi
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/applications/{uuid}/previews/{pull_request_id}/logs', function () {
|
||||
test('returns runtime logs from the selected preview container', function () {
|
||||
createPreview($this->application, 42);
|
||||
$privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->server->update(['private_key_id' => $privateKey->id]);
|
||||
Process::fake(function ($process) {
|
||||
if (str_contains($process->command, 'docker ps -a')) {
|
||||
return Process::result(output: json_encode([
|
||||
'ID' => 'preview-container',
|
||||
'Names' => "{$this->application->uuid}-pr-42",
|
||||
'Labels' => "coolify.applicationId={$this->application->id},coolify.pullRequestId=42",
|
||||
]));
|
||||
}
|
||||
if (str_contains($process->command, 'docker inspect')) {
|
||||
return Process::result(output: json_encode(['State' => ['Status' => 'running']]));
|
||||
}
|
||||
if (str_contains($process->command, 'docker logs')) {
|
||||
return Process::result(output: 'preview runtime log');
|
||||
}
|
||||
|
||||
return Process::result();
|
||||
});
|
||||
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}/previews/42/logs")
|
||||
->assertOk()
|
||||
->assertJson(['logs' => 'preview runtime log']);
|
||||
});
|
||||
|
||||
test('returns 404 when the preview does not exist', function () {
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}/previews/42/logs")
|
||||
->assertNotFound()
|
||||
->assertJson(['message' => 'Preview not found.']);
|
||||
});
|
||||
|
||||
test('rejects an invalid pull request id', function () {
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}/previews/not-a-number/logs")
|
||||
->assertUnprocessable()
|
||||
->assertJson(['message' => 'Invalid pull_request_id.']);
|
||||
});
|
||||
|
||||
test('uses the pull request id to select the preview container', function () {
|
||||
$controller = file_get_contents(app_path('Http/Controllers/Api/ApplicationsController.php'));
|
||||
$openApi = json_decode(file_get_contents(base_path('openapi.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
expect($controller)
|
||||
->toContain("\$request->route('pull_request_id')")
|
||||
->toContain('getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pullRequestId)')
|
||||
->and($openApi['paths'])
|
||||
->toHaveKey('/applications/{uuid}/previews/{pull_request_id}/logs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/v1/applications/{uuid}/previews/{pull_request_id}', function () {
|
||||
test('stores preview domain ports separately from portless public domains', function () {
|
||||
$preview = createPreview($this->application, 42);
|
||||
|
||||
Reference in New Issue
Block a user