Merge branch 'next' into service/lobe-ai-chat
This commit is contained in:
commit
ccd4e6e6d3
28 changed files with 1818 additions and 340 deletions
|
|
@ -651,4 +651,8 @@ ## Test Enforcement
|
|||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
|
||||
</laravel-boost-guidelines>
|
||||
</laravel-boost-guidelines>
|
||||
|
||||
|
||||
Random other things you should remember:
|
||||
- App\Models\Application::team must return a relationship instance., always use team()
|
||||
35
app/Events/ApplicationConfigurationChanged.php
Normal file
35
app/Events/ApplicationConfigurationChanged.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ApplicationConfigurationChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -3380,11 +3380,12 @@ private function validateDataApplications(Request $request, Server $server)
|
|||
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
|
||||
$errors = [];
|
||||
$fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
|
||||
$domain = trim($domain);
|
||||
if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
|
||||
$errors[] = 'Invalid domain: '.$domain;
|
||||
}
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
return str($domain)->lower();
|
||||
});
|
||||
if (count($errors) > 0) {
|
||||
return response()->json([
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@
|
|||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Enums\NewDatabaseTypes;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Project;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class DatabasesController extends Controller
|
||||
|
|
@ -79,13 +83,88 @@ public function databases(Request $request)
|
|||
foreach ($projects as $project) {
|
||||
$databases = $databases->merge($project->databases());
|
||||
}
|
||||
$databases = $databases->map(function ($database) {
|
||||
|
||||
$databaseIds = $databases->pluck('id')->toArray();
|
||||
|
||||
$backupConfigs = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->with('latest_log')
|
||||
->whereIn('database_id', $databaseIds)
|
||||
->get()
|
||||
->groupBy('database_id');
|
||||
|
||||
$databases = $databases->map(function ($database) use ($backupConfigs) {
|
||||
$database->backup_configs = $backupConfigs->get($database->id, collect())->values();
|
||||
|
||||
return $this->removeSensitiveData($database);
|
||||
});
|
||||
|
||||
return response()->json($databases);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get',
|
||||
description: 'Get backups details by database UUID.',
|
||||
path: '/databases/{uuid}/backups',
|
||||
operationId: 'get-database-backups-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Databases'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the database.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all backups for a database',
|
||||
content: new OA\JsonContent(
|
||||
type: 'string',
|
||||
example: 'Content is very complex. Will be implemented later.',
|
||||
),
|
||||
),
|
||||
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',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function database_backup_details_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 404);
|
||||
}
|
||||
$database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('view', $database);
|
||||
|
||||
$backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->with('executions')->where('database_id', $database->id)->get();
|
||||
|
||||
return response()->json($backupConfig);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get',
|
||||
description: 'Get database by UUID.',
|
||||
|
|
@ -248,6 +327,7 @@ public function update_by_uuid(Request $request)
|
|||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
// this check if the request is a valid json
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
|
|
@ -499,6 +579,7 @@ public function update_by_uuid(Request $request)
|
|||
$whatToDoWithDatabaseProxy = 'start';
|
||||
}
|
||||
|
||||
// Only update database fields, not backup configuration
|
||||
$database->update($request->all());
|
||||
|
||||
if ($whatToDoWithDatabaseProxy === 'start') {
|
||||
|
|
@ -512,6 +593,197 @@ public function update_by_uuid(Request $request)
|
|||
]);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update',
|
||||
description: 'Update a specific backup configuration for a given database, identified by its UUID and the backup ID',
|
||||
path: '/databases/{uuid}/backups/{scheduled_backup_uuid}',
|
||||
operationId: 'update-database-backup',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Databases'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the database.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'scheduled_backup_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the backup configuration.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Database backup configuration data',
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'save_s3' => ['type' => 'boolean', 'description' => 'Whether data is saved in s3 or not'],
|
||||
's3_storage_uuid' => ['type' => 'string', 'description' => 'S3 storage UUID'],
|
||||
'backup_now' => ['type' => 'boolean', 'description' => 'Whether to take a backup now or not'],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled or not'],
|
||||
'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'],
|
||||
'dump_all' => ['type' => 'boolean', 'description' => 'Whether all databases are dumped or not'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'],
|
||||
'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'],
|
||||
'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'],
|
||||
'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'],
|
||||
'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'],
|
||||
'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'],
|
||||
'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup in S3'],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Database backup configuration updated',
|
||||
),
|
||||
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',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_backup(Request $request)
|
||||
{
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
// this check if the request is a valid json
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'save_s3' => 'boolean',
|
||||
'backup_now' => 'boolean|nullable',
|
||||
'enabled' => 'boolean',
|
||||
'dump_all' => 'boolean',
|
||||
's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable',
|
||||
'databases_to_backup' => 'string|nullable',
|
||||
'frequency' => 'string|in:every_minute,hourly,daily,weekly,monthly,yearly',
|
||||
'database_backup_retention_amount_locally' => 'integer|min:0',
|
||||
'database_backup_retention_days_locally' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_locally' => 'integer|min:0',
|
||||
'database_backup_retention_amount_s3' => 'integer|min:0',
|
||||
'database_backup_retention_days_s3' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_s3' => 'integer|min:0',
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 404);
|
||||
}
|
||||
|
||||
// Validate scheduled_backup_uuid is provided
|
||||
if (! $request->scheduled_backup_uuid) {
|
||||
return response()->json(['message' => 'Scheduled backup UUID is required.'], 400);
|
||||
}
|
||||
|
||||
$uuid = $request->uuid;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
$database = queryDatabaseByUuidWithinTeam($uuid, $teamId);
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $database);
|
||||
|
||||
if ($request->boolean('save_s3') && ! $request->filled('s3_storage_uuid')) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['s3_storage_uuid' => ['The s3_storage_uuid field is required when save_s3 is true.']],
|
||||
], 422);
|
||||
}
|
||||
if ($request->filled('s3_storage_uuid')) {
|
||||
$existsInTeam = S3Storage::ownedByCurrentTeam()->where('uuid', $request->s3_storage_uuid)->exists();
|
||||
if (! $existsInTeam) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['s3_storage_uuid' => ['The selected S3 storage is invalid for this team.']],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id)
|
||||
->where('uuid', $request->scheduled_backup_uuid)
|
||||
->first();
|
||||
if (! $backupConfig) {
|
||||
return response()->json(['message' => 'Backup config not found.'], 404);
|
||||
}
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $backupConfigFields, ['backup_now']);
|
||||
if (! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$backupData = $request->only($backupConfigFields);
|
||||
|
||||
// Convert s3_storage_uuid to s3_storage_id
|
||||
if (isset($backupData['s3_storage_uuid'])) {
|
||||
$s3Storage = S3Storage::ownedByCurrentTeam()->where('uuid', $backupData['s3_storage_uuid'])->first();
|
||||
if ($s3Storage) {
|
||||
$backupData['s3_storage_id'] = $s3Storage->id;
|
||||
} elseif ($request->boolean('save_s3')) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['s3_storage_uuid' => ['The selected S3 storage is invalid for this team.']],
|
||||
], 422);
|
||||
}
|
||||
unset($backupData['s3_storage_uuid']);
|
||||
}
|
||||
|
||||
$backupConfig->update($backupData);
|
||||
|
||||
if ($request->backup_now) {
|
||||
dispatch(new DatabaseBackupJob($backupConfig));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Database backup configuration updated',
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create (PostgreSQL)',
|
||||
description: 'Create a new PostgreSQL database.',
|
||||
|
|
@ -1630,6 +1902,344 @@ public function delete_by_uuid(Request $request)
|
|||
]);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete backup configuration',
|
||||
description: 'Deletes a backup configuration and all its executions.',
|
||||
path: '/databases/{uuid}/backups/{scheduled_backup_uuid}',
|
||||
operationId: 'delete-backup-configuration-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Databases'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the database',
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'scheduled_backup_uuid',
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the backup configuration to delete',
|
||||
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'delete_s3',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description: 'Whether to delete all backup files from S3',
|
||||
schema: new OA\Schema(type: 'boolean', default: false)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Backup configuration deleted.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => new OA\Schema(type: 'string', example: 'Backup configuration and all executions deleted.'),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
description: 'Backup configuration not found.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => new OA\Schema(type: 'string', example: 'Backup configuration not found.'),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_backup_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
// Validate scheduled_backup_uuid is provided
|
||||
if (! $request->scheduled_backup_uuid) {
|
||||
return response()->json(['message' => 'Scheduled backup UUID is required.'], 400);
|
||||
}
|
||||
|
||||
$database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $database);
|
||||
|
||||
// Find the backup configuration by its UUID
|
||||
$backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id)
|
||||
->where('uuid', $request->scheduled_backup_uuid)
|
||||
->first();
|
||||
|
||||
if (! $backup) {
|
||||
return response()->json(['message' => 'Backup configuration not found.'], 404);
|
||||
}
|
||||
|
||||
$deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
// Get all executions for this backup configuration
|
||||
$executions = $backup->executions()->get();
|
||||
|
||||
// Delete all execution files (locally and optionally from S3)
|
||||
foreach ($executions as $execution) {
|
||||
if ($execution->filename) {
|
||||
deleteBackupsLocally($execution->filename, $database->destination->server);
|
||||
|
||||
if ($deleteS3 && $backup->s3) {
|
||||
deleteBackupsS3($execution->filename, $backup->s3);
|
||||
}
|
||||
}
|
||||
|
||||
$execution->delete();
|
||||
}
|
||||
|
||||
// Delete the backup configuration itself
|
||||
$backup->delete();
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Backup configuration and all executions deleted.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return response()->json(['message' => 'Failed to delete backup: '.$e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete backup execution',
|
||||
description: 'Deletes a specific backup execution.',
|
||||
path: '/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}',
|
||||
operationId: 'delete-backup-execution-by-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Databases'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the database',
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'scheduled_backup_uuid',
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the backup configuration',
|
||||
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'execution_uuid',
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the backup execution to delete',
|
||||
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'delete_s3',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description: 'Whether to delete the backup from S3',
|
||||
schema: new OA\Schema(type: 'boolean', default: false)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Backup execution deleted.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => new OA\Schema(type: 'string', example: 'Backup execution deleted.'),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
description: 'Backup execution not found.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => new OA\Schema(type: 'string', example: 'Backup execution not found.'),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_execution_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
// Validate parameters
|
||||
if (! $request->scheduled_backup_uuid) {
|
||||
return response()->json(['message' => 'Scheduled backup UUID is required.'], 400);
|
||||
}
|
||||
if (! $request->execution_uuid) {
|
||||
return response()->json(['message' => 'Execution UUID is required.'], 400);
|
||||
}
|
||||
|
||||
$database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $database);
|
||||
|
||||
// Find the backup configuration by its UUID
|
||||
$backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id)
|
||||
->where('uuid', $request->scheduled_backup_uuid)
|
||||
->first();
|
||||
|
||||
if (! $backup) {
|
||||
return response()->json(['message' => 'Backup configuration not found.'], 404);
|
||||
}
|
||||
|
||||
// Find the specific execution
|
||||
$execution = $backup->executions()->where('uuid', $request->execution_uuid)->first();
|
||||
if (! $execution) {
|
||||
return response()->json(['message' => 'Backup execution not found.'], 404);
|
||||
}
|
||||
|
||||
$deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
try {
|
||||
if ($execution->filename) {
|
||||
deleteBackupsLocally($execution->filename, $database->destination->server);
|
||||
|
||||
if ($deleteS3 && $backup->s3) {
|
||||
deleteBackupsS3($execution->filename, $backup->s3);
|
||||
}
|
||||
}
|
||||
|
||||
$execution->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Backup execution deleted.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['message' => 'Failed to delete backup execution: '.$e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List backup executions',
|
||||
description: 'Get all executions for a specific backup configuration.',
|
||||
path: '/databases/{uuid}/backups/{scheduled_backup_uuid}/executions',
|
||||
operationId: 'list-backup-executions',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Databases'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the database',
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'scheduled_backup_uuid',
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the backup configuration',
|
||||
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'List of backup executions',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'executions' => new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string'],
|
||||
'filename' => ['type' => 'string'],
|
||||
'size' => ['type' => 'integer'],
|
||||
'created_at' => ['type' => 'string'],
|
||||
'message' => ['type' => 'string'],
|
||||
'status' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
description: 'Backup configuration not found.',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function list_backup_executions(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
// Validate scheduled_backup_uuid is provided
|
||||
if (! $request->scheduled_backup_uuid) {
|
||||
return response()->json(['message' => 'Scheduled backup UUID is required.'], 400);
|
||||
}
|
||||
|
||||
$database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId);
|
||||
if (! $database) {
|
||||
return response()->json(['message' => 'Database not found.'], 404);
|
||||
}
|
||||
|
||||
// Find the backup configuration by its UUID
|
||||
$backup = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->where('database_id', $database->id)
|
||||
->where('uuid', $request->scheduled_backup_uuid)
|
||||
->first();
|
||||
|
||||
if (! $backup) {
|
||||
return response()->json(['message' => 'Backup configuration not found.'], 404);
|
||||
}
|
||||
|
||||
// Get all executions for this backup configuration
|
||||
$executions = $backup->executions()
|
||||
->orderBy('created_at', 'desc')
|
||||
->get()
|
||||
->map(function ($execution) {
|
||||
return [
|
||||
'uuid' => $execution->uuid,
|
||||
'filename' => $execution->filename,
|
||||
'size' => $execution->size,
|
||||
'created_at' => $execution->created_at->toIso8601String(),
|
||||
'message' => $execution->message,
|
||||
'status' => $execution->status,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'executions' => $executions,
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Start',
|
||||
description: 'Start database. `Post` request is also accepted.',
|
||||
|
|
|
|||
661
app/Http/Controllers/Api/GithubController.php
Normal file
661
app/Http/Controllers/Api/GithubController.php
Normal file
|
|
@ -0,0 +1,661 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\PrivateKey;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class GithubController extends Controller
|
||||
{
|
||||
#[OA\Post(
|
||||
summary: 'Create GitHub App',
|
||||
description: 'Create a new GitHub app.',
|
||||
path: '/github-apps',
|
||||
operationId: 'create-github-app',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['GitHub Apps'],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'GitHub app creation payload.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'Name of the GitHub app.'],
|
||||
'organization' => ['type' => 'string', 'nullable' => true, 'description' => 'Organization to associate the app with.'],
|
||||
'api_url' => ['type' => 'string', 'description' => 'API URL for the GitHub app (e.g., https://api.github.com).'],
|
||||
'html_url' => ['type' => 'string', 'description' => 'HTML URL for the GitHub app (e.g., https://github.com).'],
|
||||
'custom_user' => ['type' => 'string', 'description' => 'Custom user for SSH access (default: git).'],
|
||||
'custom_port' => ['type' => 'integer', 'description' => 'Custom port for SSH access (default: 22).'],
|
||||
'app_id' => ['type' => 'integer', 'description' => 'GitHub App ID from GitHub.'],
|
||||
'installation_id' => ['type' => 'integer', 'description' => 'GitHub Installation ID.'],
|
||||
'client_id' => ['type' => 'string', 'description' => 'GitHub OAuth App Client ID.'],
|
||||
'client_secret' => ['type' => 'string', 'description' => 'GitHub OAuth App Client Secret.'],
|
||||
'webhook_secret' => ['type' => 'string', 'description' => 'Webhook secret for GitHub webhooks.'],
|
||||
'private_key_uuid' => ['type' => 'string', 'description' => 'UUID of an existing private key for GitHub App authentication.'],
|
||||
'is_system_wide' => ['type' => 'boolean', 'description' => 'Is this app system-wide (cloud only).'],
|
||||
],
|
||||
required: ['name', 'api_url', 'html_url', 'app_id', 'installation_id', 'client_id', 'client_secret', 'private_key_uuid'],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'GitHub app created successfully.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'uuid' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'organization' => ['type' => 'string', 'nullable' => true],
|
||||
'api_url' => ['type' => 'string'],
|
||||
'html_url' => ['type' => 'string'],
|
||||
'custom_user' => ['type' => 'string'],
|
||||
'custom_port' => ['type' => 'integer'],
|
||||
'app_id' => ['type' => 'integer'],
|
||||
'installation_id' => ['type' => 'integer'],
|
||||
'client_id' => ['type' => 'string'],
|
||||
'private_key_id' => ['type' => 'integer'],
|
||||
'is_system_wide' => ['type' => 'boolean'],
|
||||
'team_id' => ['type' => 'integer'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_github_app(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$allowedFields = [
|
||||
'name',
|
||||
'organization',
|
||||
'api_url',
|
||||
'html_url',
|
||||
'custom_user',
|
||||
'custom_port',
|
||||
'app_id',
|
||||
'installation_id',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'webhook_secret',
|
||||
'private_key_uuid',
|
||||
'is_system_wide',
|
||||
];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'organization' => 'nullable|string|max:255',
|
||||
'api_url' => 'required|string|url',
|
||||
'html_url' => 'required|string|url',
|
||||
'custom_user' => 'nullable|string|max:255',
|
||||
'custom_port' => 'nullable|integer|min:1|max:65535',
|
||||
'app_id' => 'required|integer',
|
||||
'installation_id' => 'required|integer',
|
||||
'client_id' => 'required|string|max:255',
|
||||
'client_secret' => 'required|string',
|
||||
'webhook_secret' => 'required|string',
|
||||
'private_key_uuid' => 'required|string',
|
||||
'is_system_wide' => 'boolean',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify the private key belongs to the team
|
||||
$privateKey = PrivateKey::where('uuid', $request->input('private_key_uuid'))
|
||||
->where('team_id', $teamId)
|
||||
->first();
|
||||
|
||||
if (! $privateKey) {
|
||||
return response()->json([
|
||||
'message' => 'Private key not found or does not belong to your team.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'uuid' => Str::uuid(),
|
||||
'name' => $request->input('name'),
|
||||
'organization' => $request->input('organization'),
|
||||
'api_url' => $request->input('api_url'),
|
||||
'html_url' => $request->input('html_url'),
|
||||
'custom_user' => $request->input('custom_user', 'git'),
|
||||
'custom_port' => $request->input('custom_port', 22),
|
||||
'app_id' => $request->input('app_id'),
|
||||
'installation_id' => $request->input('installation_id'),
|
||||
'client_id' => $request->input('client_id'),
|
||||
'client_secret' => $request->input('client_secret'),
|
||||
'webhook_secret' => $request->input('webhook_secret'),
|
||||
'private_key_id' => $privateKey->id,
|
||||
'is_public' => false,
|
||||
'team_id' => $teamId,
|
||||
];
|
||||
|
||||
if (! isCloud()) {
|
||||
$payload['is_system_wide'] = $request->input('is_system_wide', false);
|
||||
}
|
||||
|
||||
$githubApp = GithubApp::create($payload);
|
||||
|
||||
return response()->json($githubApp, 201);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/github-apps/{github_app_id}/repositories',
|
||||
summary: 'Load Repositories for a GitHub App',
|
||||
description: 'Fetch repositories from GitHub for a given GitHub app.',
|
||||
operationId: 'load-repositories',
|
||||
tags: ['GitHub Apps'],
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'github_app_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'integer'),
|
||||
description: 'GitHub App ID'
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Repositories loaded successfully.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'repositories' => new OA\Items(
|
||||
type: 'array',
|
||||
items: new OA\Schema(type: 'object')
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function load_repositories($github_app_id)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
$githubApp = GithubApp::where('id', $github_app_id)
|
||||
->where('team_id', $teamId)
|
||||
->firstOrFail();
|
||||
|
||||
$token = generateGithubInstallationToken($githubApp);
|
||||
$repositories = collect();
|
||||
$page = 1;
|
||||
$maxPages = 100; // Safety limit: max 10,000 repositories
|
||||
|
||||
while ($page <= $maxPages) {
|
||||
$response = Http::GitHub($githubApp->api_url, $token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get('/installation/repositories', [
|
||||
'per_page' => 100,
|
||||
'page' => $page,
|
||||
]);
|
||||
|
||||
if ($response->status() !== 200) {
|
||||
return response()->json([
|
||||
'message' => $response->json()['message'] ?? 'Failed to load repositories',
|
||||
], $response->status());
|
||||
}
|
||||
|
||||
$json = $response->json();
|
||||
$repos = $json['repositories'] ?? [];
|
||||
|
||||
if (empty($repos)) {
|
||||
break; // No more repositories to load
|
||||
}
|
||||
|
||||
$repositories = $repositories->concat($repos);
|
||||
$page++;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'repositories' => $repositories->sortBy('name')->values(),
|
||||
]);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return response()->json(['message' => 'GitHub app not found'], 404);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/github-apps/{github_app_id}/repositories/{owner}/{repo}/branches',
|
||||
summary: 'Load Branches for a GitHub Repository',
|
||||
description: 'Fetch branches from GitHub for a given repository.',
|
||||
operationId: 'load-branches',
|
||||
tags: ['GitHub Apps'],
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'github_app_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'integer'),
|
||||
description: 'GitHub App ID'
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'owner',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string'),
|
||||
description: 'Repository owner'
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'repo',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string'),
|
||||
description: 'Repository name'
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Branches loaded successfully.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'branches' => new OA\Items(
|
||||
type: 'array',
|
||||
items: new OA\Schema(type: 'object')
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function load_branches($github_app_id, $owner, $repo)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
$githubApp = GithubApp::where('id', $github_app_id)
|
||||
->where('team_id', $teamId)
|
||||
->firstOrFail();
|
||||
|
||||
$token = generateGithubInstallationToken($githubApp);
|
||||
|
||||
$response = Http::GitHub($githubApp->api_url, $token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get("/repos/{$owner}/{$repo}/branches");
|
||||
|
||||
if ($response->status() !== 200) {
|
||||
return response()->json([
|
||||
'message' => 'Error loading branches from GitHub.',
|
||||
'error' => $response->json('message'),
|
||||
], $response->status());
|
||||
}
|
||||
|
||||
$branches = $response->json();
|
||||
|
||||
return response()->json([
|
||||
'branches' => $branches,
|
||||
]);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return response()->json(['message' => 'GitHub app not found'], 404);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a GitHub app.
|
||||
*/
|
||||
#[OA\Patch(
|
||||
path: '/github-apps/{github_app_id}',
|
||||
operationId: 'updateGithubApp',
|
||||
security: [
|
||||
['api_token' => []],
|
||||
],
|
||||
tags: ['GitHub Apps'],
|
||||
summary: 'Update GitHub App',
|
||||
description: 'Update an existing GitHub app.',
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'github_app_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'integer'),
|
||||
description: 'GitHub App ID'
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'GitHub App name'],
|
||||
'organization' => ['type' => 'string', 'nullable' => true, 'description' => 'GitHub organization'],
|
||||
'api_url' => ['type' => 'string', 'description' => 'GitHub API URL'],
|
||||
'html_url' => ['type' => 'string', 'description' => 'GitHub HTML URL'],
|
||||
'custom_user' => ['type' => 'string', 'description' => 'Custom user for SSH'],
|
||||
'custom_port' => ['type' => 'integer', 'description' => 'Custom port for SSH'],
|
||||
'app_id' => ['type' => 'integer', 'description' => 'GitHub App ID'],
|
||||
'installation_id' => ['type' => 'integer', 'description' => 'GitHub Installation ID'],
|
||||
'client_id' => ['type' => 'string', 'description' => 'GitHub Client ID'],
|
||||
'client_secret' => ['type' => 'string', 'description' => 'GitHub Client Secret'],
|
||||
'webhook_secret' => ['type' => 'string', 'description' => 'GitHub Webhook Secret'],
|
||||
'private_key_uuid' => ['type' => 'string', 'description' => 'Private key UUID'],
|
||||
'is_system_wide' => ['type' => 'boolean', 'description' => 'Is system wide (non-cloud instances only)'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'GitHub app updated successfully',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'GitHub app updated successfully'],
|
||||
'data' => ['type' => 'object', 'description' => 'Updated GitHub app data'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||
new OA\Response(response: 404, description: 'GitHub app not found'),
|
||||
new OA\Response(response: 422, description: 'Validation error'),
|
||||
]
|
||||
)]
|
||||
public function update_github_app(Request $request, $github_app_id)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
$githubApp = GithubApp::where('id', $github_app_id)
|
||||
->where('team_id', $teamId)
|
||||
->firstOrFail();
|
||||
|
||||
// Define allowed fields for update
|
||||
$allowedFields = [
|
||||
'name',
|
||||
'organization',
|
||||
'api_url',
|
||||
'html_url',
|
||||
'custom_user',
|
||||
'custom_port',
|
||||
'app_id',
|
||||
'installation_id',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'webhook_secret',
|
||||
'private_key_uuid',
|
||||
];
|
||||
|
||||
if (! isCloud()) {
|
||||
$allowedFields[] = 'is_system_wide';
|
||||
}
|
||||
|
||||
$payload = $request->only($allowedFields);
|
||||
|
||||
// Validate the request
|
||||
$rules = [];
|
||||
if (isset($payload['name'])) {
|
||||
$rules['name'] = 'string';
|
||||
}
|
||||
if (isset($payload['organization'])) {
|
||||
$rules['organization'] = 'nullable|string';
|
||||
}
|
||||
if (isset($payload['api_url'])) {
|
||||
$rules['api_url'] = 'url';
|
||||
}
|
||||
if (isset($payload['html_url'])) {
|
||||
$rules['html_url'] = 'url';
|
||||
}
|
||||
if (isset($payload['custom_user'])) {
|
||||
$rules['custom_user'] = 'string';
|
||||
}
|
||||
if (isset($payload['custom_port'])) {
|
||||
$rules['custom_port'] = 'integer|min:1|max:65535';
|
||||
}
|
||||
if (isset($payload['app_id'])) {
|
||||
$rules['app_id'] = 'integer';
|
||||
}
|
||||
if (isset($payload['installation_id'])) {
|
||||
$rules['installation_id'] = 'integer';
|
||||
}
|
||||
if (isset($payload['client_id'])) {
|
||||
$rules['client_id'] = 'string';
|
||||
}
|
||||
if (isset($payload['client_secret'])) {
|
||||
$rules['client_secret'] = 'string';
|
||||
}
|
||||
if (isset($payload['webhook_secret'])) {
|
||||
$rules['webhook_secret'] = 'string';
|
||||
}
|
||||
if (isset($payload['private_key_uuid'])) {
|
||||
$rules['private_key_uuid'] = 'string|uuid';
|
||||
}
|
||||
if (! isCloud() && isset($payload['is_system_wide'])) {
|
||||
$rules['is_system_wide'] = 'boolean';
|
||||
}
|
||||
|
||||
$validator = customApiValidator($payload, $rules);
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation error',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Handle private_key_uuid -> private_key_id conversion
|
||||
if (isset($payload['private_key_uuid'])) {
|
||||
$privateKey = PrivateKey::where('team_id', $teamId)
|
||||
->where('uuid', $payload['private_key_uuid'])
|
||||
->first();
|
||||
|
||||
if (! $privateKey) {
|
||||
return response()->json([
|
||||
'message' => 'Private key not found or does not belong to your team',
|
||||
], 404);
|
||||
}
|
||||
|
||||
unset($payload['private_key_uuid']);
|
||||
$payload['private_key_id'] = $privateKey->id;
|
||||
}
|
||||
|
||||
// Update the GitHub app
|
||||
$githubApp->update($payload);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'GitHub app updated successfully',
|
||||
'data' => $githubApp,
|
||||
]);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return response()->json([
|
||||
'message' => 'GitHub app not found',
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a GitHub app.
|
||||
*/
|
||||
#[OA\Delete(
|
||||
path: '/github-apps/{github_app_id}',
|
||||
operationId: 'deleteGithubApp',
|
||||
security: [
|
||||
['api_token' => []],
|
||||
],
|
||||
tags: ['GitHub Apps'],
|
||||
summary: 'Delete GitHub App',
|
||||
description: 'Delete a GitHub app if it\'s not being used by any applications.',
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'github_app_id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'integer'),
|
||||
description: 'GitHub App ID'
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'GitHub app deleted successfully',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'GitHub app deleted successfully'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(response: 401, description: 'Unauthorized'),
|
||||
new OA\Response(response: 404, description: 'GitHub app not found'),
|
||||
new OA\Response(
|
||||
response: 409,
|
||||
description: 'Conflict - GitHub app is in use',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'This GitHub app is being used by 5 application(s). Please delete all applications first.'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_github_app($github_app_id)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
$githubApp = GithubApp::where('id', $github_app_id)
|
||||
->where('team_id', $teamId)
|
||||
->firstOrFail();
|
||||
|
||||
// Check if the GitHub app is being used by any applications
|
||||
if ($githubApp->applications->isNotEmpty()) {
|
||||
$count = $githubApp->applications->count();
|
||||
|
||||
return response()->json([
|
||||
'message' => "This GitHub app is being used by {$count} application(s). Please delete all applications first.",
|
||||
], 409);
|
||||
}
|
||||
|
||||
$githubApp->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'GitHub app deleted successfully',
|
||||
]);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return response()->json([
|
||||
'message' => 'GitHub app not found',
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -179,6 +179,8 @@ public function members_by_id(Request $request)
|
|||
$members = $team->members;
|
||||
$members->makeHidden([
|
||||
'pivot',
|
||||
'email_change_code',
|
||||
'email_change_code_expires_at',
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
|
|
@ -264,6 +266,8 @@ public function current_team_members(Request $request)
|
|||
$team = auth()->user()->currentTeam();
|
||||
$team->members->makeHidden([
|
||||
'pivot',
|
||||
'email_change_code',
|
||||
'email_change_code_expires_at',
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Events\ApplicationConfigurationChanged;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
|
|
@ -147,6 +148,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
private Collection $saved_outputs;
|
||||
|
||||
private ?string $secrets_hash_key = null;
|
||||
|
||||
private ?string $full_healthcheck_url = null;
|
||||
|
||||
private string $serverUser = 'root';
|
||||
|
|
@ -2712,22 +2715,28 @@ private function generate_build_env_variables()
|
|||
if ($this->application->build_pack === 'nixpacks') {
|
||||
$variables = collect($this->nixpacks_plan_json->get('variables'));
|
||||
} else {
|
||||
// Generate environment variables for build process (filters by is_buildtime = true)
|
||||
$this->generate_env_variables();
|
||||
$variables = collect([])->merge($this->env_args);
|
||||
}
|
||||
|
||||
// Check if build secrets are enabled and BuildKit is supported
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
$this->generate_build_secrets($variables);
|
||||
$this->build_args = '';
|
||||
} else {
|
||||
// Fall back to traditional build args
|
||||
$secrets_hash = '';
|
||||
if ($variables->isNotEmpty()) {
|
||||
$secrets_hash = $this->generate_secrets_hash($variables);
|
||||
}
|
||||
|
||||
$this->build_args = $variables->map(function ($value, $key) {
|
||||
$value = escapeshellarg($value);
|
||||
|
||||
return "--build-arg {$key}={$value}";
|
||||
});
|
||||
|
||||
if ($secrets_hash) {
|
||||
$this->build_args->push("--build-arg COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2746,13 +2755,18 @@ private function generate_docker_env_flags_for_secrets()
|
|||
return '';
|
||||
}
|
||||
|
||||
return $variables
|
||||
$secrets_hash = $this->generate_secrets_hash($variables);
|
||||
$env_flags = $variables
|
||||
->map(function ($env) {
|
||||
$escaped_value = escapeshellarg($env->real_value);
|
||||
|
||||
return "-e {$env->key}={$escaped_value}";
|
||||
})
|
||||
->implode(' ');
|
||||
|
||||
$env_flags .= " -e COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}";
|
||||
|
||||
return $env_flags;
|
||||
}
|
||||
|
||||
private function generate_build_secrets(Collection $variables)
|
||||
|
|
@ -2768,6 +2782,36 @@ private function generate_build_secrets(Collection $variables)
|
|||
return "--secret id={$key},env={$key}";
|
||||
})
|
||||
->implode(' ');
|
||||
|
||||
$this->build_secrets .= ' --secret id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH';
|
||||
}
|
||||
|
||||
private function generate_secrets_hash($variables)
|
||||
{
|
||||
if (! $this->secrets_hash_key) {
|
||||
$this->secrets_hash_key = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
if ($variables instanceof Collection) {
|
||||
$secrets_string = $variables
|
||||
->mapWithKeys(function ($value, $key) {
|
||||
return [$key => $value];
|
||||
})
|
||||
->sortKeys()
|
||||
->map(function ($value, $key) {
|
||||
return "{$key}={$value}";
|
||||
})
|
||||
->implode('|');
|
||||
} else {
|
||||
$secrets_string = $variables
|
||||
->map(function ($env) {
|
||||
return "{$env->key}={$env->real_value}";
|
||||
})
|
||||
->sort()
|
||||
->implode('|');
|
||||
}
|
||||
|
||||
return hash_hmac('sha256', $secrets_string, $this->secrets_hash_key);
|
||||
}
|
||||
|
||||
private function add_build_env_variables_to_dockerfile()
|
||||
|
|
@ -2809,6 +2853,12 @@ private function add_build_env_variables_to_dockerfile()
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($envs->isNotEmpty()) {
|
||||
$secrets_hash = $this->generate_secrets_hash($envs);
|
||||
$dockerfile->splice(1, 0, ["ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"]);
|
||||
}
|
||||
|
||||
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"),
|
||||
|
|
@ -2850,6 +2900,9 @@ private function modify_dockerfile_for_secrets($dockerfile_path)
|
|||
// Generate mount strings for all secrets
|
||||
$mountStrings = $variables->map(fn ($env) => "--mount=type=secret,id={$env->key},env={$env->key}")->implode(' ');
|
||||
|
||||
// Add mount for the secrets hash to ensure cache invalidation
|
||||
$mountStrings .= ' --mount=type=secret,id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH';
|
||||
|
||||
$modified = false;
|
||||
$dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) {
|
||||
$trimmed = ltrim($line);
|
||||
|
|
@ -3186,6 +3239,9 @@ private function next(string $status)
|
|||
queue_next_deployment($this->application);
|
||||
|
||||
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||
ray($this->application->team()->id);
|
||||
event(new ApplicationConfigurationChanged($this->application->team()->id));
|
||||
|
||||
if (! $this->only_this_server) {
|
||||
$this->deploy_to_additional_destinations();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -547,9 +547,10 @@ public function submit($showToaster = true)
|
|||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
$domain = trim($domain);
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
return str($domain)->lower();
|
||||
});
|
||||
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
|
|
|
|||
|
|
@ -143,7 +143,13 @@ public function loadBranches()
|
|||
|
||||
protected function loadBranchByPage()
|
||||
{
|
||||
$response = Http::withToken($this->token)->get("{$this->github_app->api_url}/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches?per_page=100&page={$this->page}");
|
||||
$response = Http::GitHub($this->github_app->api_url, $this->token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get("/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches", [
|
||||
'per_page' => 100,
|
||||
'page' => $this->page,
|
||||
]);
|
||||
$json = $response->json();
|
||||
if ($response->status() !== 200) {
|
||||
return $this->dispatch('error', $json['message']);
|
||||
|
|
|
|||
|
|
@ -41,9 +41,10 @@ public function submit()
|
|||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
$domain = trim($domain);
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
return str($domain)->lower();
|
||||
});
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
|
|
|
|||
|
|
@ -149,9 +149,10 @@ public function submit()
|
|||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
$domain = trim($domain);
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
return str($domain)->lower();
|
||||
});
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,15 @@ class ConfigurationChecker extends Component
|
|||
|
||||
public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
|
||||
|
||||
protected $listeners = ['configurationChanged'];
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ApplicationConfigurationChanged" => 'configurationChanged',
|
||||
'configurationChanged' => 'configurationChanged',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class Metrics extends Component
|
|||
{
|
||||
public $resource;
|
||||
|
||||
public $chartId = 'container-cpu';
|
||||
public $chartId = 'metrics';
|
||||
|
||||
public $data;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public function mount()
|
|||
|
||||
public function getConfigurationFilePathProperty()
|
||||
{
|
||||
return $this->server->proxyPath().'/docker-compose.yml';
|
||||
return $this->server->proxyPath().'docker-compose.yml';
|
||||
}
|
||||
|
||||
public function changeProxy()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,21 @@ class ScheduledDatabaseBackup extends BaseModel
|
|||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return ScheduledDatabaseBackup::whereRelation('team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||
{
|
||||
return ScheduledDatabaseBackup::whereRelation('team', 'id', $teamId)->orderBy('name');
|
||||
}
|
||||
|
||||
public function team()
|
||||
{
|
||||
return $this->belongsTo(Team::class);
|
||||
}
|
||||
|
||||
public function database(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
|||
$dangerousChars = [
|
||||
';', '|', '&', '$', '`', '(', ')', '{', '}',
|
||||
'[', ']', '<', '>', '\n', '\r', '\0', '"', "'",
|
||||
'\\', '!', '?', '*', '~', '^', '%', '=', '+',
|
||||
'\\', '!', '?', '*', '^', '%', '=', '+',
|
||||
'#', // Comment character that could hide commands
|
||||
];
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
|||
}
|
||||
|
||||
// Validate SSH URL format (git@host:user/repo.git)
|
||||
if (! preg_match('/^git@[a-zA-Z0-9\.\-]+:[a-zA-Z0-9\-_\/\.]+$/', $value)) {
|
||||
if (! preg_match('/^git@[a-zA-Z0-9\.\-]+:[a-zA-Z0-9\-_\/\.~]+$/', $value)) {
|
||||
$fail('The :attribute is not a valid SSH repository URL.');
|
||||
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -135,7 +135,13 @@ function getPermissionsPath(GithubApp $source)
|
|||
|
||||
function loadRepositoryByPage(GithubApp $source, string $token, int $page)
|
||||
{
|
||||
$response = Http::withToken($token)->get("{$source->api_url}/installation/repositories?per_page=100&page={$page}");
|
||||
$response = Http::GitHub($source->api_url, $token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get('/installation/repositories', [
|
||||
'per_page' => 100,
|
||||
'page' => $page,
|
||||
]);
|
||||
$json = $response->json();
|
||||
if ($response->status() !== 200) {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -84,64 +84,6 @@ function () use ($source, $dest, $server) {
|
|||
);
|
||||
}
|
||||
|
||||
function transfer_file_to_container(string $content, string $container_path, string $deployment_uuid, Server $server, bool $throwError = true): ?string
|
||||
{
|
||||
$temp_file = tempnam(sys_get_temp_dir(), 'coolify_env_');
|
||||
|
||||
try {
|
||||
// Write content to temporary file
|
||||
file_put_contents($temp_file, $content);
|
||||
|
||||
// Generate unique filename for server transfer
|
||||
$server_temp_file = '/tmp/coolify_env_'.uniqid().'_'.$deployment_uuid;
|
||||
|
||||
// Transfer file to server
|
||||
instant_scp($temp_file, $server_temp_file, $server, $throwError);
|
||||
|
||||
// Ensure parent directory exists in container, then copy file
|
||||
$parent_dir = dirname($container_path);
|
||||
$commands = [];
|
||||
if ($parent_dir !== '.' && $parent_dir !== '/') {
|
||||
$commands[] = executeInDocker($deployment_uuid, "mkdir -p \"$parent_dir\"");
|
||||
}
|
||||
$commands[] = "docker cp $server_temp_file $deployment_uuid:$container_path";
|
||||
$commands[] = "rm -f $server_temp_file"; // Cleanup server temp file
|
||||
|
||||
return instant_remote_process_with_timeout($commands, $server, $throwError);
|
||||
|
||||
} finally {
|
||||
// Always cleanup local temp file
|
||||
if (file_exists($temp_file)) {
|
||||
unlink($temp_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function transfer_file_to_server(string $content, string $server_path, Server $server, bool $throwError = true): ?string
|
||||
{
|
||||
$temp_file = tempnam(sys_get_temp_dir(), 'coolify_env_');
|
||||
|
||||
try {
|
||||
// Write content to temporary file
|
||||
file_put_contents($temp_file, $content);
|
||||
|
||||
// Ensure parent directory exists on server
|
||||
$parent_dir = dirname($server_path);
|
||||
if ($parent_dir !== '.' && $parent_dir !== '/') {
|
||||
instant_remote_process_with_timeout(["mkdir -p \"$parent_dir\""], $server, $throwError);
|
||||
}
|
||||
|
||||
// Transfer file directly to server destination
|
||||
return instant_scp($temp_file, $server_path, $server, $throwError);
|
||||
|
||||
} finally {
|
||||
// Always cleanup local temp file
|
||||
if (file_exists($temp_file)) {
|
||||
unlink($temp_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function instant_remote_process_with_timeout(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
|
||||
{
|
||||
$command = $command instanceof Collection ? $command->toArray() : $command;
|
||||
|
|
|
|||
|
|
@ -634,10 +634,14 @@ function getTopLevelNetworks(Service|Application $resource)
|
|||
$definedNetwork = collect([$resource->uuid]);
|
||||
$services = collect($services)->map(function ($service, $_) use ($topLevelNetworks, $definedNetwork) {
|
||||
$serviceNetworks = collect(data_get($service, 'networks', []));
|
||||
$hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false;
|
||||
$networkMode = data_get($service, 'network_mode');
|
||||
|
||||
// Only add 'networks' key if 'network_mode' is not 'host'
|
||||
if (! $hasHostNetworkMode) {
|
||||
$hasValidNetworkMode =
|
||||
$networkMode === 'host' ||
|
||||
(is_string($networkMode) && (str_starts_with($networkMode, 'service:') || str_starts_with($networkMode, 'container:')));
|
||||
|
||||
// Only add 'networks' key if 'network_mode' is not 'host' or does not start with 'service:' or 'container:'
|
||||
if (! $hasValidNetworkMode) {
|
||||
// Collect/create/update networks
|
||||
if ($serviceNetworks->count() > 0) {
|
||||
foreach ($serviceNetworks as $networkName => $networkDetails) {
|
||||
|
|
@ -1272,7 +1276,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
|||
$serviceNetworks = collect(data_get($service, 'networks', []));
|
||||
$serviceVariables = collect(data_get($service, 'environment', []));
|
||||
$serviceLabels = collect(data_get($service, 'labels', []));
|
||||
$hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false;
|
||||
$networkMode = data_get($service, 'network_mode');
|
||||
|
||||
$hasValidNetworkMode =
|
||||
$networkMode === 'host' ||
|
||||
(is_string($networkMode) && (str_starts_with($networkMode, 'service:') || str_starts_with($networkMode, 'container:')));
|
||||
|
||||
if ($serviceLabels->count() > 0) {
|
||||
$removedLabels = collect([]);
|
||||
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
|
||||
|
|
@ -1383,7 +1392,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
|||
$savedService->ports = $collectedPorts->implode(',');
|
||||
$savedService->save();
|
||||
|
||||
if (! $hasHostNetworkMode) {
|
||||
if (! $hasValidNetworkMode) {
|
||||
// Add Coolify specific networks
|
||||
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
|
||||
return $value == $definedNetwork;
|
||||
|
|
|
|||
|
|
@ -6,10 +6,31 @@ @utility apexcharts-tooltip-title {
|
|||
@apply hidden!;
|
||||
}
|
||||
|
||||
@utility apexcharts-grid-borders {
|
||||
@apply dark:hidden!;
|
||||
}
|
||||
|
||||
@utility apexcharts-xaxistooltip {
|
||||
@apply hidden!;
|
||||
}
|
||||
|
||||
@utility apexcharts-tooltip-custom {
|
||||
@apply bg-white dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 rounded-lg shadow-lg p-3 text-sm;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
@utility apexcharts-tooltip-custom-value {
|
||||
@apply text-neutral-700 dark:text-neutral-300 mb-1;
|
||||
}
|
||||
|
||||
@utility apexcharts-tooltip-value-bold {
|
||||
@apply font-bold text-black dark:text-white;
|
||||
}
|
||||
|
||||
@utility apexcharts-tooltip-custom-title {
|
||||
@apply text-xs text-neutral-500 dark:text-neutral-400 font-medium;
|
||||
}
|
||||
|
||||
@utility input-sticky {
|
||||
@apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
'content' => null,
|
||||
'checkboxes' => [],
|
||||
'actions' => [],
|
||||
'warningMessage' => null,
|
||||
'confirmWithText' => true,
|
||||
'confirmationText' => 'Confirm Deletion',
|
||||
'confirmationLabel' => 'Please confirm the execution of the actions by entering the Name below',
|
||||
|
|
@ -228,7 +229,7 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
|
|||
<div x-show="step === 2">
|
||||
<div class="p-4 mb-4 text-white border-l-4 border-red-500 bg-error" role="alert">
|
||||
<p class="font-bold">Warning</p>
|
||||
<p>This operation is permanent and cannot be undone. Please think again before proceeding!
|
||||
<p>{!! $warningMessage ?: 'This operation is permanent and cannot be undone. Please think again before proceeding!' !!}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-4">The following actions will be performed:</div>
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@
|
|||
@endphp
|
||||
<title>{{ $name }}{{ $title ?? 'Coolify' }}</title>
|
||||
@env('local')
|
||||
<link rel="icon" href="{{ asset('coolify-logo-dev-transparent.png') }}" type="image/x-icon" />
|
||||
<link rel="icon" href="{{ asset('coolify-logo-dev-transparent.png') }}" type="image/png" />
|
||||
@else
|
||||
<link rel="icon" href="{{ asset('coolify-logo.svg') }}" type="image/x-icon" />
|
||||
<link rel="icon" href="{{ asset('coolify-logo.svg') }}" type="image/svg+xml" />
|
||||
@endenv
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
||||
|
|
@ -138,7 +138,8 @@
|
|||
}
|
||||
}
|
||||
let theme = localStorage.theme
|
||||
let baseColor = '#FCD452'
|
||||
let cpuColor = '#1e90ff'
|
||||
let ramColor = '#00ced1'
|
||||
let textColor = '#ffffff'
|
||||
let editorBackground = '#181818'
|
||||
let editorTheme = 'blackboard'
|
||||
|
|
@ -149,12 +150,14 @@ function checkTheme() {
|
|||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
if (theme == 'dark') {
|
||||
baseColor = '#FCD452'
|
||||
cpuColor = '#1e90ff'
|
||||
ramColor = '#00ced1'
|
||||
textColor = '#ffffff'
|
||||
editorBackground = '#181818'
|
||||
editorTheme = 'blackboard'
|
||||
} else {
|
||||
baseColor = 'black'
|
||||
cpuColor = '#1e90ff'
|
||||
ramColor = '#00ced1'
|
||||
textColor = '#000000'
|
||||
editorBackground = '#ffffff'
|
||||
editorTheme = null
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ class="flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto bg-white dark:
|
|||
])>
|
||||
<span x-show="showTimestamps" class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>
|
||||
<span @class([
|
||||
'text-coollabs dark:text-warning' => $line['hidden'],
|
||||
'text-success dark:text-warning' => $line['hidden'],
|
||||
'text-red-500' => $line['stderr'],
|
||||
'font-bold' => isset($line['command']) && $line['command'],
|
||||
'whitespace-pre-wrap',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
@if ($isLocked)
|
||||
<div class="flex flex-1 w-full gap-2">
|
||||
<x-forms.input disabled id="key" />
|
||||
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="icon my-1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
<path d="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6z" />
|
||||
<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4" />
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
<div>
|
||||
<div class="flex items-center gap-2 ">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Metrics</h2>
|
||||
</div>
|
||||
<div class="pb-4">Basic metrics for your container.</div>
|
||||
@if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose')
|
||||
<div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div>
|
||||
@elseif(!$resource->destination->server->isMetricsEnabled())
|
||||
<div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div>
|
||||
<div> Go to <a class="underline dark:text-white"
|
||||
href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to
|
||||
enable
|
||||
it.</div>
|
||||
@else
|
||||
@if (!str($resource->status)->contains('running'))
|
||||
<div class="alert alert-warning">Metrics are only available when this resource is running!</div>
|
||||
<div class="pb-4">Basic metrics for your application container.</div>
|
||||
<div>
|
||||
@if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose')
|
||||
<div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div>
|
||||
@elseif(!$resource->destination->server->isMetricsEnabled())
|
||||
<div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div>
|
||||
<div>Go to <a class="underline dark:text-white" href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to enable it.</div>
|
||||
@else
|
||||
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
|
||||
@if (!str($resource->status)->contains('running'))
|
||||
<div class="alert alert-warning">Metrics are only available when the application container is running!</div>
|
||||
@else
|
||||
<div>
|
||||
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
|
||||
<option value="5">5 minutes (live)</option>
|
||||
<option value="10">10 minutes (live)</option>
|
||||
<option value="30">30 minutes</option>
|
||||
|
|
@ -26,7 +25,7 @@
|
|||
</x-forms.select>
|
||||
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()"
|
||||
class="pt-5">
|
||||
<h4>CPU (%)</h4>
|
||||
<h4>CPU Usage</h4>
|
||||
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
||||
|
||||
<script>
|
||||
|
|
@ -34,6 +33,7 @@ class="pt-5">
|
|||
const optionsServerCpu = {
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: 2,
|
||||
},
|
||||
chart: {
|
||||
height: '150px',
|
||||
|
|
@ -52,7 +52,7 @@ class="pt-5">
|
|||
},
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
|
|
@ -68,74 +68,90 @@ class="pt-5">
|
|||
enabled: false,
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: '',
|
||||
},
|
||||
colors: [baseColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
},
|
||||
series: [{
|
||||
name: "CPU %",
|
||||
data: []
|
||||
}],
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
color: textColor,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
marker: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: '',
|
||||
},
|
||||
colors: [cpuColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
},
|
||||
series: [{
|
||||
name: "CPU %",
|
||||
data: []
|
||||
}],
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
color: textColor,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
marker: {
|
||||
show: false,
|
||||
},
|
||||
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
||||
const value = series[seriesIndex][dataPointIndex];
|
||||
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
||||
const date = new Date(timestamp);
|
||||
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
||||
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
||||
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
||||
date.getUTCFullYear() + '-' +
|
||||
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getUTCDate()).padStart(2, '0');
|
||||
return '<div class="apexcharts-tooltip-custom">' +
|
||||
'<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
|
||||
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
|
||||
serverCpuChart.render();
|
||||
document.addEventListener('livewire:init', () => {
|
||||
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
|
||||
checkTheme();
|
||||
serverCpuChart.updateOptions({
|
||||
series: [{
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
colors: [baseColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: true,
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
color: textColor,
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), optionsServerCpu);
|
||||
serverCpuChart.render();
|
||||
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
|
||||
checkTheme();
|
||||
serverCpuChart.updateOptions({
|
||||
series: [{
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
colors: [cpuColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: true,
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
},
|
||||
formatter: function(value) {
|
||||
return Math.round(value) + ' %';
|
||||
}
|
||||
}
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
color: textColor,
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<h3>Memory (MB)</h3>
|
||||
<h4>Memory Usage</h4>
|
||||
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
||||
|
||||
<script>
|
||||
|
|
@ -143,6 +159,7 @@ class="pt-5">
|
|||
const optionsServerMemory = {
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: 2,
|
||||
},
|
||||
chart: {
|
||||
height: '150px',
|
||||
|
|
@ -161,7 +178,7 @@ class="pt-5">
|
|||
},
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
|
|
@ -177,81 +194,99 @@ class="pt-5">
|
|||
enabled: false,
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: '',
|
||||
},
|
||||
colors: [baseColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: "Memory (MB)",
|
||||
data: []
|
||||
}],
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
color: textColor,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
marker: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: '',
|
||||
},
|
||||
colors: [ramColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: "Memory (MB)",
|
||||
data: []
|
||||
}],
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
color: textColor,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
marker: {
|
||||
show: false,
|
||||
},
|
||||
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
||||
const value = series[seriesIndex][dataPointIndex];
|
||||
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
||||
const date = new Date(timestamp);
|
||||
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
||||
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
||||
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
||||
date.getUTCFullYear() + '-' +
|
||||
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getUTCDate()).padStart(2, '0');
|
||||
return '<div class="apexcharts-tooltip-custom">' +
|
||||
'<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + ' MB</span></div>' +
|
||||
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
|
||||
optionsServerMemory);
|
||||
serverMemoryChart.render();
|
||||
document.addEventListener('livewire:init', () => {
|
||||
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
|
||||
checkTheme();
|
||||
serverMemoryChart.updateOptions({
|
||||
series: [{
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
colors: [baseColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
min: 0,
|
||||
show: true,
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
color: textColor,
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
const serverMemoryChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-memory`),
|
||||
optionsServerMemory);
|
||||
serverMemoryChart.render();
|
||||
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
|
||||
checkTheme();
|
||||
serverMemoryChart.updateOptions({
|
||||
series: [{
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
colors: [ramColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
min: 0,
|
||||
show: true,
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
},
|
||||
formatter: function(value) {
|
||||
return Math.round(value) + ' MB';
|
||||
}
|
||||
}
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
color: textColor,
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<x-server.sidebar :server="$server" activeMenu="metrics" />
|
||||
<div class="w-full">
|
||||
<h2>Metrics</h2>
|
||||
<div class="pb-4">Basic metrics for your container.</div>
|
||||
<div class="pb-4">Basic metrics for your server.</div>
|
||||
@if ($server->isMetricsEnabled())
|
||||
<div @if ($poll) wire:poll.5000ms='pollData' @endif x-init="$wire.loadData()">
|
||||
<x-forms.select label="Interval" wire:change="setInterval" id="interval">
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
<option value="10080">1 week</option>
|
||||
<option value="43200">30 days</option>
|
||||
</x-forms.select>
|
||||
<h4 class="pt-4">CPU (%)</h4>
|
||||
<h4 class="pt-4">CPU Usage</h4>
|
||||
<div wire:ignore id="{!! $chartId !!}-cpu"></div>
|
||||
|
||||
<script>
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
const optionsServerCpu = {
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: 2,
|
||||
},
|
||||
chart: {
|
||||
height: '150px',
|
||||
|
|
@ -45,7 +46,7 @@
|
|||
},
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
|
|
@ -61,16 +62,16 @@
|
|||
enabled: false,
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: '',
|
||||
},
|
||||
colors: [baseColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
},
|
||||
series: [{
|
||||
name: 'CPU %',
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: '',
|
||||
},
|
||||
colors: [cpuColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
},
|
||||
series: [{
|
||||
name: 'CPU %',
|
||||
data: []
|
||||
}],
|
||||
noData: {
|
||||
|
|
@ -79,12 +80,27 @@
|
|||
color: textColor,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
marker: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
marker: {
|
||||
show: false,
|
||||
},
|
||||
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
||||
const value = series[seriesIndex][dataPointIndex];
|
||||
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
||||
const date = new Date(timestamp);
|
||||
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
||||
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
||||
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
||||
date.getUTCFullYear() + '-' +
|
||||
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getUTCDate()).padStart(2, '0');
|
||||
return '<div class="apexcharts-tooltip-custom">' +
|
||||
'<div class="apexcharts-tooltip-custom-value">CPU: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
|
||||
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
|
|
@ -95,11 +111,11 @@
|
|||
document.addEventListener('livewire:init', () => {
|
||||
Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => {
|
||||
checkTheme();
|
||||
serverCpuChart.updateOptions({
|
||||
series: [{
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
colors: [baseColor],
|
||||
serverCpuChart.updateOptions({
|
||||
series: [{
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
colors: [cpuColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
|
|
@ -109,15 +125,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: true,
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
show: true,
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
},
|
||||
formatter: function(value) {
|
||||
return Math.round(value) + ' %';
|
||||
}
|
||||
}
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
|
|
@ -130,7 +149,7 @@
|
|||
</script>
|
||||
|
||||
<div>
|
||||
<h4>Memory (%)</h4>
|
||||
<h4>Memory Usage</h4>
|
||||
<div wire:ignore id="{!! $chartId !!}-memory"></div>
|
||||
|
||||
<script>
|
||||
|
|
@ -138,6 +157,7 @@
|
|||
const optionsServerMemory = {
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: 2,
|
||||
},
|
||||
chart: {
|
||||
height: '150px',
|
||||
|
|
@ -156,7 +176,7 @@
|
|||
},
|
||||
},
|
||||
animations: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
|
|
@ -172,15 +192,15 @@
|
|||
enabled: false,
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: '',
|
||||
},
|
||||
colors: [baseColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
show: true,
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: '',
|
||||
},
|
||||
colors: [ramColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
|
|
@ -196,12 +216,27 @@
|
|||
color: textColor,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
marker: {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
marker: {
|
||||
show: false,
|
||||
},
|
||||
custom: function({ series, seriesIndex, dataPointIndex, w }) {
|
||||
const value = series[seriesIndex][dataPointIndex];
|
||||
const timestamp = w.globals.seriesX[seriesIndex][dataPointIndex];
|
||||
const date = new Date(timestamp);
|
||||
const timeString = String(date.getUTCHours()).padStart(2, '0') + ':' +
|
||||
String(date.getUTCMinutes()).padStart(2, '0') + ':' +
|
||||
String(date.getUTCSeconds()).padStart(2, '0') + ', ' +
|
||||
date.getUTCFullYear() + '-' +
|
||||
String(date.getUTCMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(date.getUTCDate()).padStart(2, '0');
|
||||
return '<div class="apexcharts-tooltip-custom">' +
|
||||
'<div class="apexcharts-tooltip-custom-value">Memory: <span class="apexcharts-tooltip-value-bold">' + value + '%</span></div>' +
|
||||
'<div class="apexcharts-tooltip-custom-title">' + timeString + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
|
|
@ -212,11 +247,11 @@
|
|||
document.addEventListener('livewire:init', () => {
|
||||
Livewire.on('refreshChartData-{!! $chartId !!}-memory', (chartData) => {
|
||||
checkTheme();
|
||||
serverMemoryChart.updateOptions({
|
||||
series: [{
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
colors: [baseColor],
|
||||
serverMemoryChart.updateOptions({
|
||||
series: [{
|
||||
data: chartData[0].seriesData,
|
||||
}],
|
||||
colors: [ramColor],
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
|
|
@ -226,16 +261,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
min: 0,
|
||||
show: true,
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
min: 0,
|
||||
show: true,
|
||||
labels: {
|
||||
show: true,
|
||||
style: {
|
||||
colors: textColor,
|
||||
},
|
||||
formatter: function(value) {
|
||||
return Math.round(value) + ' %';
|
||||
}
|
||||
}
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
|
|
|
|||
|
|
@ -7,23 +7,23 @@
|
|||
<div class="flex items-center gap-2">
|
||||
<h2>Configuration</h2>
|
||||
@if ($server->proxy->status === 'exited' || $server->proxy->status === 'removing')
|
||||
<x-forms.button canGate="update" :canResource="$server" wire:click.prevent="changeProxy">Switch
|
||||
Proxy</x-forms.button>
|
||||
@can('update', $server)
|
||||
<x-modal-confirmation title="Confirm Proxy Switching?" buttonTitle="Switch Proxy"
|
||||
submitAction="changeProxy" :actions="['Custom proxy configurations may be reset to their default settings.']"
|
||||
warningMessage="This operation may cause issues. Please refer to the guide <a href='https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies' target='_blank' class='underline text-white'>switching between proxies</a> before proceeding!"
|
||||
step2ButtonText="Switch Proxy" :confirmWithText="false" :confirmWithPassword="false">
|
||||
</x-modal-confirmation>
|
||||
@endcan
|
||||
@else
|
||||
<x-forms.button canGate="update" :canResource="$server" disabled
|
||||
wire:click.prevent="changeProxy">Switch Proxy</x-forms.button>
|
||||
<x-forms.button canGate="update" :canResource="$server"
|
||||
wire:click="$dispatch('error', 'Currently running proxy must be stopped before switching proxy')">Switch
|
||||
Proxy</x-forms.button>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$server" type="submit">Save</x-forms.button>
|
||||
</div>
|
||||
<div class="pb-4 "> <svg class="inline-flex w-6 h-6 mr-2 dark:text-warning" viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16" />
|
||||
</svg>Before switching proxies, please read <a class="underline dark:text-white"
|
||||
href="https://coolify.io/docs/knowledge-base/server/proxies#switch-between-proxies">this</a>.
|
||||
</div>
|
||||
<div class="subtitle">Configure your proxy settings and advanced options.</div>
|
||||
<h3>Advanced</h3>
|
||||
<div class="pb-4 w-96">
|
||||
<div class="pb-6 w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$server"
|
||||
helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.<br>For applications, labels needs to be regenerated manually. <br>Resources needs to be restarted."
|
||||
id="server.settings.generate_exact_labels"
|
||||
|
|
@ -36,10 +36,31 @@
|
|||
id="redirectUrl" label="Redirect to (optional)" />
|
||||
@endif
|
||||
</div>
|
||||
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
|
||||
<h3>Traefik</h3>
|
||||
@elseif ($server->proxyType() === 'CADDY')
|
||||
<h3>Caddy</h3>
|
||||
@php
|
||||
$proxyTitle =
|
||||
$server->proxyType() === ProxyTypes::TRAEFIK->value
|
||||
? 'Traefik (Coolify Proxy)'
|
||||
: 'Caddy (Coolify Proxy)';
|
||||
@endphp
|
||||
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value || $server->proxyType() === 'CADDY')
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>{{ $proxyTitle }}</h3>
|
||||
@if ($proxySettings)
|
||||
@can('update', $server)
|
||||
<x-modal-confirmation title="Reset Proxy Configuration?"
|
||||
buttonTitle="Reset Configuration" submitAction="resetProxyConfiguration"
|
||||
:actions="[
|
||||
'Reset proxy configuration to default settings',
|
||||
'All custom configurations will be lost',
|
||||
'Custom ports and entrypoints will be removed',
|
||||
]" confirmationText="{{ $server->name }}"
|
||||
confirmationLabel="Please confirm by entering the server name below"
|
||||
shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration"
|
||||
:confirmWithPassword="false" :confirmWithText="true">
|
||||
</x-modal-confirmation>
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@if (
|
||||
$server->proxy->last_applied_settings &&
|
||||
|
|
@ -53,25 +74,11 @@
|
|||
</div>
|
||||
<div wire:loading.remove wire:target="loadProxyConfiguration">
|
||||
@if ($proxySettings)
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<div class="flex flex-col gap-2 pt-2">
|
||||
<x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor
|
||||
monacoEditorLanguage="yaml"
|
||||
label="Configuration file ({{ $this->configurationFilePath }})" name="proxySettings"
|
||||
id="proxySettings" rows="30" />
|
||||
@can('update', $server)
|
||||
<x-modal-confirmation title="Reset Proxy Configuration?"
|
||||
buttonTitle="Reset configuration to default" isErrorButton
|
||||
submitAction="resetProxyConfiguration" :actions="[
|
||||
'Reset proxy configuration to default settings',
|
||||
'All custom configurations will be lost',
|
||||
'Custom ports and entrypoints will be removed',
|
||||
]"
|
||||
confirmationText="{{ $server->name }}"
|
||||
confirmationLabel="Please confirm by entering the server name below"
|
||||
shortConfirmationLabel="Server Name" step2ButtonText="Reset Configuration"
|
||||
:confirmWithPassword="false" :confirmWithText="true">
|
||||
</x-modal-confirmation>
|
||||
@endcan
|
||||
label="Configuration file ( {{ $this->configurationFilePath }} )"
|
||||
name="proxySettings" id="proxySettings" rows="30" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use App\Http\Controllers\Api\ApplicationsController;
|
||||
use App\Http\Controllers\Api\DatabasesController;
|
||||
use App\Http\Controllers\Api\DeployController;
|
||||
use App\Http\Controllers\Api\GithubController;
|
||||
use App\Http\Controllers\Api\OtherController;
|
||||
use App\Http\Controllers\Api\ProjectController;
|
||||
use App\Http\Controllers\Api\ResourcesController;
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
});
|
||||
|
||||
Route::post('/feedback', [OtherController::class, 'feedback']);
|
||||
|
||||
Route::group([
|
||||
'middleware' => ['auth:sanctum', 'api.ability:write'],
|
||||
'prefix' => 'v1',
|
||||
|
|
@ -102,6 +104,12 @@
|
|||
Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::post('/github-apps', [GithubController::class, 'create_github_app'])->middleware(['api.ability:write']);
|
||||
Route::patch('/github-apps/{github_app_id}', [GithubController::class, 'update_github_app'])->middleware(['api.ability:write']);
|
||||
Route::delete('/github-apps/{github_app_id}', [GithubController::class, 'delete_github_app'])->middleware(['api.ability:write']);
|
||||
Route::get('/github-apps/{github_app_id}/repositories', [GithubController::class, 'load_repositories'])->middleware(['api.ability:read']);
|
||||
Route::get('/github-apps/{github_app_id}/repositories/{owner}/{repo}/branches', [GithubController::class, 'load_branches'])->middleware(['api.ability:read']);
|
||||
|
||||
Route::get('/databases', [DatabasesController::class, 'databases'])->middleware(['api.ability:read']);
|
||||
Route::post('/databases/postgresql', [DatabasesController::class, 'create_database_postgresql'])->middleware(['api.ability:write']);
|
||||
Route::post('/databases/mysql', [DatabasesController::class, 'create_database_mysql'])->middleware(['api.ability:write']);
|
||||
|
|
@ -113,8 +121,13 @@
|
|||
Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']);
|
||||
Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']);
|
||||
Route::get('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions', [DatabasesController::class, 'list_backup_executions'])->middleware(['api.ability:read']);
|
||||
Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']);
|
||||
Route::patch('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'update_backup'])->middleware(['api.ability:write']);
|
||||
Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
|
||||
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']);
|
||||
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', [DatabasesController::class, 'delete_execution_by_uuid'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:write']);
|
||||
|
|
|
|||
Loading…
Reference in a new issue