feat: initial api endpoints

feat: server resources are now looks better
This commit is contained in:
Andras Bacsai 2024-02-16 21:56:38 +01:00
parent 3539e4dce9
commit b8708f086e
24 changed files with 318 additions and 142 deletions

View File

@ -18,8 +18,7 @@ class Deploy extends Controller
{
public function deploy(Request $request)
{
$token = auth()->user()->currentAccessToken();
$teamId = data_get($token, 'team_id');
$teamId = get_team_id_from_token();
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project as ModelsProject;
use Illuminate\Http\Request;
class Project extends Controller
{
public function projects(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$projects = ModelsProject::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
return response()->json($projects);
}
public function project_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
return response()->json($project);
}
public function environment_details(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
$environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
return response()->json($environment);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Server as ModelsServer;
use Illuminate\Http\Request;
class Server extends Controller
{
public function servers(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) {
$server['is_reachable'] = $server->settings->is_reachable;
$server['is_usable'] = $server->settings->is_usable;
return $server;
});
ray($servers);
return response()->json($servers);
}
public function server_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (is_null($server)) {
return response()->json(['error' => 'Server not found.'], 404);
}
$server->load(['settings']);
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
'id' => $resource->id,
'uuid' => $resource->uuid,
'name' => $resource->name,
'type' => $resource->type(),
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
return response()->json($server);
}
}

View File

@ -104,7 +104,7 @@ public function mount()
'environment_name' => data_get($service, 'environment.name'),
'service_uuid' => data_get($service, 'uuid')
]);
$service->status = serviceStatus($service);
$service->status = $service->status();
}
return $service;
});

View File

@ -0,0 +1,30 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Resources extends Component
{
use AuthorizesRequests;
public ?Server $server = null;
public $parameters = [];
public function mount() {
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.index');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.resources');
}
}

View File

@ -26,7 +26,6 @@ public function applications()
{
return $this->hasMany(Application::class);
}
public function postgresqls()
{
return $this->hasMany(StandalonePostgresql::class);

View File

@ -6,7 +6,10 @@
class ProjectSetting extends Model
{
protected $fillable = [
'project_id'
];
protected $guarded = [];
public function project()
{
return $this->belongsTo(Project::class);
}
}

View File

@ -245,6 +245,8 @@ public function databases()
$mysqls = data_get($standaloneDocker, 'mysqls', collect([]));
$mariadbs = data_get($standaloneDocker, 'mariadbs', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
})->filter(function ($item) {
return data_get($item,'name') === 'coolify-db';
})->flatten();
}
public function applications()

View File

@ -28,6 +28,48 @@ public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function status() {
$foundRunning = false;
$isDegraded = false;
$foundRestaring = false;
$applications = $this->applications;
$databases = $this->databases;
foreach ($applications as $application) {
if ($application->exclude_from_status) {
continue;
}
if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($application->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
foreach ($databases as $database) {
if ($database->exclude_from_status) {
continue;
}
if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($database->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
if ($foundRestaring) {
return 'degraded';
}
if ($foundRunning && !$isDegraded) {
return 'running';
} else if ($foundRunning && $isDegraded) {
return 'degraded';
} else if (!$foundRunning && !$isDegraded) {
return 'exited';
}
return 'exited';
}
public function extraFields()
{
$fields = collect([]);

View File

@ -82,6 +82,9 @@ public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project() {
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@ -85,6 +85,9 @@ public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project() {
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@ -82,6 +82,9 @@ public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project() {
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@ -82,6 +82,10 @@ public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project()
{
return data_get($this, 'environment.project');
}
public function link()
{
if (data_get($this, 'environment.project.uuid')) {

View File

@ -77,6 +77,10 @@ public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project()
{
return data_get($this, 'environment.project');
}
public function team()
{
return data_get($this, 'environment.project.team');

View File

@ -16,7 +16,7 @@ public function __construct(
public Service $service,
public string $complexStatus = 'exited',
) {
$this->complexStatus = serviceStatus($service);
$this->complexStatus = $service->status();
}
/**

View File

@ -0,0 +1,7 @@
<?php
function get_team_id_from_token()
{
$token = auth()->user()->currentAccessToken();
return data_get($token, 'team_id');
}

View File

@ -21,49 +21,6 @@ function replaceVariables($variable)
return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', '');
}
function serviceStatus(Service $service)
{
$foundRunning = false;
$isDegraded = false;
$foundRestaring = false;
$applications = $service->applications;
$databases = $service->databases;
foreach ($applications as $application) {
if ($application->exclude_from_status) {
continue;
}
if (Str::of($application->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($application->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
foreach ($databases as $database) {
if ($database->exclude_from_status) {
continue;
}
if (Str::of($database->status)->startsWith('running')) {
$foundRunning = true;
} else if (Str::of($database->status)->startsWith('restarting')) {
$foundRestaring = true;
} else {
$isDegraded = true;
}
}
if ($foundRestaring) {
return 'degraded';
}
if ($foundRunning && !$isDegraded) {
return 'running';
} else if ($foundRunning && $isDegraded) {
return 'degraded';
} else if (!$foundRunning && !$isDegraded) {
return 'exited';
}
return 'exited';
}
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService, bool $isInit = false)
{
// TODO: make this async

View File

@ -18,6 +18,12 @@
]) }}">
<button>General</button>
</a>
<a class="{{ request()->routeIs('server.resources') ? 'text-white' : '' }}"
href="{{ route('server.resources', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Resources</button>
</a>
<a class="{{ request()->routeIs('server.private-key') ? 'text-white' : '' }}"
href="{{ route('server.private-key', [
'server_uuid' => data_get($parameters, 'server_uuid'),

View File

@ -5,7 +5,7 @@
</a>
<x-services.links />
<div class="flex-1"></div>
@if (serviceStatus($service) === 'degraded')
@if ($service->status() === 'degraded')
<button wire:click='deploy' onclick="startService.showModal()"
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@ -26,7 +26,7 @@ class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"
Stop
</button>
@endif
@if (serviceStatus($service) === 'running')
@if ($service->status() === 'running')
<button wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@ -47,7 +47,7 @@ class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"
Stop
</button>
@endif
@if (serviceStatus($service) === 'exited')
@if ($service->status() === 'exited')
<button wire:click='stop(true)'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 " viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">

View File

@ -1,11 +1,15 @@
@props(['status', 'showRefreshButton' => true])
@if (str($status)->startsWith('running'))
<x-status.running :status="$status" />
@elseif(str($status)->startsWith('restarting') || str($status)->startsWith('starting') || str($status)->startsWith('degraded'))
@elseif(str($status)->startsWith('restarting') ||
str($status)->startsWith('starting') ||
str($status)->startsWith('degraded'))
<x-status.restarting :status="$status" />
@else
<x-status.stopped :status="$status"/>
@endif
@if (!str($status)->contains('exited'))
@if (!str($status)->contains('exited') && $showRefreshButton)
<button title="Refresh Status" wire:click='check_status(true)' class="mx-1 hover:fill-white fill-warning">
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path

View File

@ -22,37 +22,33 @@
@endif
@if ($resource->link())
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
<div class="w-40">
@if ($resource->type() === 'service')
<x-status.services :service="$resource" />
@else
<x-status.index :status="$resource->status" :showRefreshButton="false" :showIcon="false" />
@endif
</div>
<div>{{ $resource->name }}<span
class="px-2 text-xs">({{ str($resource->type())->headline() }})</span></div>
</a>
@else
<div class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
<div class="w-40">
@if ($resource->type() === 'service')
<x-status.services :service="$resource" />
@else
<x-status.index :status="$resource->status" :showRefreshButton="false" :showIcon="false" />
@endif
</div>
<div>{{ $resource->name }}<span
class="px-2 text-xs">({{ str($resource->type())->headline() }})</span></div>
</div>
@endif
@empty
@endforelse
</div>
@else
<div class="flex flex-col">
@forelse ($server->definedResources() as $resource)
@if ($loop->first)
<h3 class="pt-4">Resources</h3>
@endif
@if ($resource->link())
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</a>
@else
<div class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</div>
@endif
@empty
@endforelse
</div>
@endif
</div>

View File

@ -0,0 +1,46 @@
<div>
<x-server.navbar :server="$server" :parameters="$parameters" />
<h2>Resources</h2>
<div class="pb-4 title">Here you can find all resources for this server.</div>
<div class="flex flex-col">
<div class="flex flex-col">
<div class="overflow-x-auto">
<div class="inline-block min-w-full">
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-coolgray-400">
<thead>
<tr class="text-neutral-500">
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Project Name</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Name</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Type</th>
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-coolgray-400">
@forelse ($server->definedResources()->sortBy('name',SORT_NATURAL) as $resource)
<tr class="text-white bg-coolblack hover:bg-coolgray-100">
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ data_get($resource->project(), 'name') }}
</td>
<td class="px-5 py-4 text-sm whitespace-nowrap"> <a class=""
href="{{ $resource->link() }}">{{ $resource->name }} </a></td>
<td class="px-5 py-4 text-sm whitespace-nowrap">
{{ str($resource->type())->headline() }}</td>
<td class="px-5 py-4 text-sm font-medium whitespace-nowrap">
@if ($resource->type() === 'service')
<x-status.services :service="$resource" />
@else
<x-status.index :status="$resource->status" :showRefreshButton="false" />
@endif
</td>
</tr>
@empty
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,36 +1,11 @@
<?php
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Service\StartService;
use App\Http\Controllers\Api\Deploy;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use App\Http\Controllers\Api\Project;
use App\Http\Controllers\Api\Server;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Visus\Cuid2\Cuid2;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
$middlewares = ['auth:sanctum'];
if (isDev()) {
$middlewares = [];
}
Route::get('/health', function () {
return 'OK';
@ -45,44 +20,41 @@
}
return response()->json(['message' => 'Feedback sent.'], 200);
});
// Route::group([
// 'middleware' => $middlewares,
// 'prefix' => 'v1'
// ], function () {
// Route::get('/deployments', function () {
// return ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->get([
// "id",
// "server_id",
// "status"
// ])->groupBy("server_id")->map(function ($item) {
// return $item;
// })->toArray();
// });
// });
Route::group([
'middleware' => ['auth:sanctum'],
'prefix' => 'v1'
], function () {
Route::get('/version', function () {
return response(config('version'));
});
Route::get('/deploy', [Deploy::class, 'deploy']);
Route::get('/servers', [Server::class, 'servers']);
Route::get('/server/{uuid}', [Server::class, 'server_by_uuid']);
Route::get('/projects', [Project::class, 'projects']);
Route::get('/project/{uuid}', [Project::class, 'project_by_uuid']);
Route::get('/project/{uuid}/{environment_name}', [Project::class, 'environment_details']);
});
Route::middleware(['throttle:5'])->group(function () {
Route::get('/unsubscribe/{token}', function () {
try {
$token = request()->token;
$email = decrypt($token);
if (!User::whereEmail($email)->exists()) {
return redirect(RouteServiceProvider::HOME);
}
if (User::whereEmail($email)->first()->marketing_emails === false) {
return 'You have already unsubscribed from marketing emails.';
}
User::whereEmail($email)->update(['marketing_emails' => false]);
return 'You have been unsubscribed from marketing emails.';
} catch (\Throwable $e) {
return 'Something went wrong. Please try again or contact support.';
}
})->name('unsubscribe.marketing.emails');
});
Route::get('/{any}', function () {
return response()->json(['error' => 'Not found.'], 404);
})->where('any', '.*');
// Route::middleware(['throttle:5'])->group(function () {
// Route::get('/unsubscribe/{token}', function () {
// try {
// $token = request()->token;
// $email = decrypt($token);
// if (!User::whereEmail($email)->exists()) {
// return redirect(RouteServiceProvider::HOME);
// }
// if (User::whereEmail($email)->first()->marketing_emails === false) {
// return 'You have already unsubscribed from marketing emails.';
// }
// User::whereEmail($email)->update(['marketing_emails' => false]);
// return 'You have been unsubscribed from marketing emails.';
// } catch (\Throwable $e) {
// return 'Something went wrong. Please try again or contact support.';
// }
// })->name('unsubscribe.marketing.emails');
// });

View File

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Api\Server as ApiServer;
use App\Models\GitlabApp;
use App\Models\PrivateKey;
use App\Models\Server;
@ -61,12 +62,13 @@
use App\Livewire\Server\Index as ServerIndex;
use App\Livewire\Server\Create as ServerCreate;
use App\Livewire\Server\Show as ServerShow;
use App\Livewire\Server\Resources as ResourcesShow;
use App\Livewire\Server\Destination\Show as DestinationShow;
use App\Livewire\Server\LogDrains;
use App\Livewire\Server\PrivateKey\Show as PrivateKeyShow;
use App\Livewire\Server\Proxy\Show as ProxyShow;
use App\Livewire\Server\Proxy\Logs as ProxyLogs;
use App\Livewire\Source\Github\Change as GitHubChange;
use App\Livewire\Subscription\Index as SubscriptionIndex;
@ -173,6 +175,7 @@
Route::prefix('server/{server_uuid}')->group(function () {
Route::get('/', ServerShow::class)->name('server.show');
Route::get('/resources', ResourcesShow::class)->name('server.resources');
Route::get('/proxy', ProxyShow::class)->name('server.proxy');
Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs');
Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key');