Merge branch 'main' of github.com:pelican-dev/panel

This commit is contained in:
Lance Pioch 2024-05-15 13:24:44 -04:00
commit b513366f35
70 changed files with 2885 additions and 804 deletions

View File

@ -13,12 +13,8 @@ LOG_LEVEL=debug
DB_CONNECTION=sqlite
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
CACHE_STORE=file
QUEUE_CONNECTION=sync
QUEUE_CONNECTION=database
SESSION_DRIVER=file
HASHIDS_SALT=

2
.github/FUNDING.yml vendored
View File

@ -1,2 +1,2 @@
github: pelican-dev
custom: [https://buy.stripe.com/14kdU99SI4UT7ni9AB, https://buy.stripe.com/14kaHXc0Q9b9372eUU]
open_collective: pelican-panel

View File

@ -11,23 +11,20 @@ class AppSettingsCommand extends Command
{
use EnvironmentWriterTrait;
public const CACHE_DRIVERS = [
'redis' => 'Redis',
'memcached' => 'Memcached',
'file' => 'Filesystem (recommended)',
'redis' => 'Redis',
];
public const SESSION_DRIVERS = [
'redis' => 'Redis',
'memcached' => 'Memcached',
'database' => 'MySQL Database',
'file' => 'Filesystem (recommended)',
'redis' => 'Redis',
'database' => 'MySQL Database',
'cookie' => 'Cookie',
];
public const QUEUE_DRIVERS = [
'database' => 'MySQL Database (recommended)',
'redis' => 'Redis',
'database' => 'MySQL Database',
'sync' => 'Sync (recommended)',
];
protected $description = 'Configure basic environment settings for the Panel.';
@ -86,7 +83,7 @@ class AppSettingsCommand extends Command
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
);
$selected = config('queue.default', 'sync');
$selected = config('queue.default', 'database');
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
'Queue Driver',
self::QUEUE_DRIVERS,

View File

@ -31,7 +31,7 @@ class EmailSettingsCommand extends Command
*/
public function handle(): void
{
$this->variables['MAIL_DRIVER'] = $this->option('driver') ?? $this->choice(
$this->variables['MAIL_MAILER'] = $this->option('driver') ?? $this->choice(
trans('command/messages.environment.mail.ask_driver'),
[
'log' => 'Log',
@ -41,10 +41,10 @@ class EmailSettingsCommand extends Command
'mandrill' => 'Mandrill',
'postmark' => 'Postmark',
],
'smtp',
env('MAIL_MAILER', env('MAIL_DRIVER', 'smtp')),
);
$method = 'setup' . studly_case($this->variables['MAIL_DRIVER']) . 'DriverVariables';
$method = 'setup' . studly_case($this->variables['MAIL_MAILER']) . 'DriverVariables';
if (method_exists($this, $method)) {
$this->{$method}();
}

View File

@ -7,9 +7,7 @@ use Filament\Notifications\Notification;
use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Container\Container;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

View File

@ -13,6 +13,10 @@ class ApiKeyResource extends Resource
protected static ?string $model = ApiKey::class;
protected static ?string $label = 'API Key';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count();
}
protected static ?string $navigationIcon = 'tabler-key';
public static function canEdit($record): bool

View File

@ -10,6 +10,11 @@ class DatabaseHostResource extends Resource
{
protected static ?string $model = DatabaseHost::class;
public static function getNavigationBadge(): ?string
{
return static::getModel()::count();
}
protected static ?string $label = 'Databases';
protected static ?string $navigationIcon = 'tabler-database';

View File

@ -22,21 +22,38 @@ class CreateDatabaseHost extends CreateRecord
{
return $form
->schema([
Section::make()->schema([
Section::make()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('host')
->columnSpan(2)
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
->required()
->live()
->debounce(500)
->live(onBlur: true)
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
->maxLength(191),
Forms\Components\TextInput::make('port')
->columnSpan(1)
->helperText('The port that MySQL is running on for this host.')
->required()
->numeric()
->default(3306)
->minValue(0)
->maxValue(65535),
Forms\Components\TextInput::make('max_databases')
->label('Max databases')
->helpertext('Blank is unlimited.')
->numeric(),
Forms\Components\TextInput::make('name')
->label('Display Name')
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
->required()
->maxLength(60),
Forms\Components\TextInput::make('username')
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
->required()
@ -47,25 +64,17 @@ class CreateDatabaseHost extends CreateRecord
->revealable()
->maxLength(191)
->required(),
Forms\Components\TextInput::make('name')
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
->required()
->maxLength(60),
Forms\Components\Select::make('node_id')
->searchable()
->preload()
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
->label('Linked Node')
->relationship('node', 'name'),
])->columns([
'default' => 1,
'lg' => 2,
]),
]);
}
protected function mutateFormDataBeforeSave(array $data): array
protected function mutateFormDataBeforeCreate(array $data): array
{
if (isset($data['password'])) {
$data['password'] = encrypt($data['password']);
@ -73,4 +82,17 @@ class CreateDatabaseHost extends CreateRecord
return $data;
}
protected function getHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -17,21 +17,37 @@ class EditDatabaseHost extends EditRecord
{
return $form
->schema([
Section::make()->schema([
Section::make()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('host')
->columnSpan(2)
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
->required()
->live()
->debounce(500)
->live(onBlur: true)
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
->maxLength(191),
Forms\Components\TextInput::make('port')
->columnSpan(1)
->helperText('The port that MySQL is running on for this host.')
->required()
->numeric()
->default(3306)
->minValue(0)
->maxValue(65535),
Forms\Components\TextInput::make('max_databases')
->label('Max databases')
->helpertext('Blank is unlimited.')
->numeric(),
Forms\Components\TextInput::make('name')
->label('Display Name')
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
->required()
->maxLength(60),
Forms\Components\TextInput::make('username')
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
->required()
@ -42,19 +58,12 @@ class EditDatabaseHost extends EditRecord
->revealable()
->maxLength(191)
->required(),
Forms\Components\TextInput::make('name')
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
->required()
->maxLength(60),
Forms\Components\Select::make('node_id')
->searchable()
->preload()
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
->label('Linked Node')
->relationship('node', 'name'),
])->columns([
'default' => 1,
'lg' => 2,
]),
]);
}
@ -63,6 +72,7 @@ class EditDatabaseHost extends EditRecord
{
return [
Actions\DeleteAction::make(),
$this->getSaveFormAction()->formId('form'),
];
}
@ -74,4 +84,16 @@ class EditDatabaseHost extends EditRecord
return $data;
}
protected function getFormActions(): array
{
return [];
}
public function getRelationManagers(): array
{
return [
DatabaseHostResource\RelationManagers\DatabasesRelationManager::class,
];
}
}

View File

@ -12,6 +12,8 @@ class ListDatabaseHosts extends ListRecords
{
protected static string $resource = DatabaseHostResource::class;
protected ?string $heading = 'Database Hosts';
public function table(Table $table): Table
{
return $table
@ -48,7 +50,7 @@ class ListDatabaseHosts extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make('create')->label('New Database Host'),
];
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Filament\Resources\DatabaseHostResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class DatabasesRelationManager extends RelationManager
{
protected static string $relationship = 'databases';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('database')->columnSpanFull(),
Forms\Components\TextInput::make('username'),
Forms\Components\TextInput::make('password')->default('Soon™'),
Forms\Components\TextInput::make('remote')->label('Connections From'),
Forms\Components\TextInput::make('max_connections'),
Forms\Components\TextInput::make('JDBC')->label('JDBC Connection String')->columnSpanFull()->default('Soon™'),
Forms\Components\TextInput::make('created_at'),
Forms\Components\TextInput::make('updated_at'),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('servers')
->columns([
Tables\Columns\TextColumn::make('database'),
Tables\Columns\TextColumn::make('username'),
//Tables\Columns\TextColumn::make('password'),
Tables\Columns\TextColumn::make('remote'),
Tables\Columns\TextColumn::make('server_id')
->label('Belongs To'),
// TODO ->url(route('filament.admin.resources.servers.edit', ['record', ''])),
Tables\Columns\TextColumn::make('max_connections'),
Tables\Columns\TextColumn::make('created_at'),
])
->actions([
Tables\Actions\DeleteAction::make(),
Tables\Actions\ViewAction::make(),
//Tables\Actions\EditAction::make(),
]);
}
}

View File

@ -10,6 +10,11 @@ class DatabaseResource extends Resource
{
protected static ?string $model = Database::class;
public static function getNavigationBadge(): ?string
{
return static::getModel()::count();
}
protected static ?string $navigationIcon = 'tabler-database';
protected static bool $shouldRegisterNavigation = false;

View File

@ -10,6 +10,10 @@ class EggResource extends Resource
{
protected static ?string $model = Egg::class;
public static function getNavigationBadge(): ?string
{
return static::getModel()::count();
}
protected static ?string $navigationIcon = 'tabler-eggs';
protected static ?string $recordTitleAttribute = 'name';

View File

@ -25,12 +25,16 @@ class EditEgg extends EditRecord
Forms\Components\TextInput::make('name')
->required()
->maxLength(191)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1])
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
Forms\Components\TextInput::make('uuid')
->label('Egg UUID')
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'),
Forms\Components\TextInput::make('id')
->label('Egg ID')
->disabled(),
Forms\Components\Textarea::make('description')
->rows(3)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
@ -203,6 +207,19 @@ class EditEgg extends EditRecord
->color('primary')
// TODO uses old admin panel export service
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])),
$this->getSaveFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
public function getRelationManagers(): array
{
return [
EggResource\RelationManagers\ServersRelationManager::class,
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Filament\Resources\EggResource\RelationManagers;
use App\Models\Server;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class ServersRelationManager extends RelationManager
{
protected static string $relationship = 'servers';
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('servers')
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('user.username')
->label('Owner')
->icon('tabler-user')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->sortable(),
Tables\Columns\TextColumn::make('name')
->icon('tabler-brand-docker')
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
->sortable(),
Tables\Columns\TextColumn::make('node.name')
->icon('tabler-server-2')
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
Tables\Columns\TextColumn::make('image')
->label('Docker Image'),
Tables\Columns\SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
]);
}
}

View File

@ -10,6 +10,11 @@ class MountResource extends Resource
{
protected static ?string $model = Mount::class;
public static function getNavigationBadge(): ?string
{
return static::getModel()::count();
}
protected static ?string $navigationIcon = 'tabler-layers-linked';
public static function getRelations(): array

View File

@ -3,7 +3,6 @@
namespace App\Filament\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use App\Services\Servers\ServerCreationService;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;

View File

@ -98,6 +98,12 @@ class EditMount extends EditRecord
{
return [
Actions\DeleteAction::make(),
$this->getSaveFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -11,6 +11,11 @@ class NodeResource extends Resource
{
protected static ?string $model = Node::class;
public static function getNavigationBadge(): ?string
{
return static::getModel()::count();
}
protected static ?string $navigationIcon = 'tabler-server-2';
protected static ?string $recordTitleAttribute = 'name';

View File

@ -4,6 +4,7 @@ namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use Filament\Forms;
use Filament\Forms\Components\Tabs;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\HtmlString;
@ -17,13 +18,19 @@ class CreateNode extends CreateRecord
public function form(Forms\Form $form): Forms\Form
{
return $form
return $form->schema([
Tabs::make('Tabs')
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tabs\Tab::make('Basic Settings')
->icon('tabler-server')
->schema([
Forms\Components\TextInput::make('fqdn')
->columnSpan(2)
@ -176,19 +183,132 @@ class CreateNode extends CreateRecord
'https' => 'tabler-lock',
])
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Forms\Components\Textarea::make('description')
->label('strings.description')
->hidden()
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 4,
]),
Tabs\Tab::make('Advanced Settings')
->icon('tabler-server-cog')
->schema([
Forms\Components\TextInput::make('upload_size')
->label('Upload Limit')
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\ToggleButtons::make('public')
->label('Automatic Allocation')->inline()
->default(true)
->columnSpan(1)
->options([
true => 'Yes',
false => 'No',
])
->rows(5),
Forms\Components\Hidden::make('skipValidation')->default(true),
->colors([
true => 'success',
false => 'danger',
]),
Forms\Components\ToggleButtons::make('maintenance_mode')
->label('Maintenance Mode')->inline()
->columnSpan(1)
->default(false)
->hinticon('tabler-question-mark')
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
->options([
true => 'Enable',
false => 'Disable',
])
->colors([
true => 'danger',
false => 'success',
]),
Forms\Components\TagsInput::make('tags')
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented')
->columnSpan(1),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label('Overallocate')->inlineLabel()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix('MiB')
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
]),
]),
]);
}

View File

@ -32,13 +32,293 @@ class EditNode extends EditRecord
->tabs([
Tabs\Tab::make('Basic Settings')
->icon('tabler-server')
->schema((new CreateNode())->form($form)->getComponents()),
// Tabs\Tab::make('Advanced Settings')
// ->icon('tabler-server-cog')
// ->schema([
// Forms\Components\Placeholder::make('Coming soon!'),
// ]),
Tabs\Tab::make('Configuration')
->schema([
Forms\Components\TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
->live(debounce: 1500)
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) {
if (is_ip($state)) {
if (request()->isSecure()) {
return '
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
You must use a domain name, because you cannot get SSL certificates for IP Addresses
';
}
return '';
}
return "
This is the domain name that points to your node's IP Address.
If you've already set up this, you can verify it by checking the next field!
";
})
->hintColor('danger')
->hint(function ($state) {
if (is_ip($state) && request()->isSecure()) {
return 'You cannot connect to an IP Address over SSL';
}
return '';
})
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
[$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) {
$set('name', $subdomain);
}
if (!$state || is_ip($state)) {
$set('dns', null);
return;
}
$validRecords = gethostbynamel($state);
if ($validRecords) {
$set('dns', true);
$set('ip', collect($validRecords)->first());
return;
}
$set('dns', false);
})
->maxLength(191),
Forms\Components\TextInput::make('ip')
->disabled()
->hidden(),
Forms\Components\ToggleButtons::make('dns')
->label('DNS Record Check')
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
->disabled()
->inline()
->default(null)
->hint(fn (Forms\Get $get) => $get('ip'))
->hintColor('success')
->options([
true => 'Valid',
false => 'Invalid',
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
Forms\Components\TextInput::make('daemon_listen')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->label(trans('strings.port'))
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
->minValue(0)
->maxValue(65536)
->default(8080)
->required()
->integer(),
Forms\Components\TextInput::make('name')
->label('Display Name')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->required()
->regex('/[a-zA-Z0-9_\.\- ]+/')
->helperText('This name is for display only and can be changed later.')
->maxLength(100),
Forms\Components\ToggleButtons::make('scheme')
->label('Communicate over SSL')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->required()
->inline()
->helperText(function (Forms\Get $get) {
if (request()->isSecure()) {
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
}
if (is_ip($get('fqdn'))) {
return 'An IP address cannot use SSL.';
}
return '';
})
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
->options([
'http' => 'HTTP',
'https' => 'HTTPS (SSL)',
])
->colors([
'http' => 'warning',
'https' => 'success',
])
->icons([
'http' => 'tabler-lock-open-off',
'https' => 'tabler-lock',
])
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
Tabs\Tab::make('Advanced Settings')
->icon('tabler-server-cog')
->schema([
Forms\Components\TextInput::make('id')
->label('Node ID')
->disabled(),
Forms\Components\TextInput::make('uuid')
->label('Node UUID')
->hintAction(CopyAction::make())
->columnSpan(2)
->disabled(),
Forms\Components\TagsInput::make('tags')
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented')
->columnSpan(1),
Forms\Components\ToggleButtons::make('public')
->label('Automatic Allocation')->inline()
->columnSpan(1)
->options([
true => 'Yes',
false => 'No',
])
->colors([
true => 'success',
false => 'danger',
]),
Forms\Components\ToggleButtons::make('maintenance_mode')
->label('Maintenance Mode')->inline()
->columnSpan(1)
->hinticon('tabler-question-mark')
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
->options([
true => 'Enable',
false => 'Disable',
])
->colors([
true => 'danger',
false => 'success',
]),
Forms\Components\TextInput::make('upload_size')
->label('Upload Limit')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->columnStart(4)->columnSpan(1)
->numeric()->required()
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label('Overallocate')->inlineLabel()
->required()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix('MiB')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->required()
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
]),
Tabs\Tab::make('Configuration File')
->icon('tabler-code')
->schema([
Forms\Components\Placeholder::make('instructions')
@ -66,18 +346,17 @@ class EditNode extends EditRecord
return $data;
}
protected function getSteps(): array
protected function getFormActions(): array
{
return [
];
return [];
}
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
->disabled(fn (Node $node) => $node->servers()->count() > 0)
->label(fn (Node $node) => $node->servers()->count() > 0 ? 'Node Has Servers' : 'Delete'),
$this->getSaveFormAction()->formId('form'),
];
}

View File

@ -42,15 +42,15 @@ class ListNodes extends ListRecords
->visibleFrom('sm')
->icon('tabler-device-desktop-analytics')
->numeric()
->suffix(' GB')
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
->suffix(' GiB')
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
->sortable(),
Tables\Columns\TextColumn::make('disk')
->visibleFrom('sm')
->icon('tabler-file')
->numeric()
->suffix(' GB')
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
->suffix(' GiB')
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
->sortable(),
Tables\Columns\IconColumn::make('scheme')
->visibleFrom('xl')

View File

@ -43,10 +43,13 @@ class AllocationsRelationManager extends RelationManager
Tables\Columns\TextColumn::make('server.name')
->label('Server')
->icon('tabler-brand-docker')
->searchable()
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
Tables\Columns\TextColumn::make('ip_alias')
Tables\Columns\TextInputColumn::make('ip_alias')
->searchable()
->label('Alias'),
Tables\Columns\TextColumn::make('ip')
Tables\Columns\TextInputColumn::make('ip')
->searchable()
->label('IP'),
Tables\Columns\TextColumn::make('port')
->searchable()
@ -126,6 +129,8 @@ class AllocationsRelationManager extends RelationManager
$ports = $sortedPorts;
}
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
if ($update) {
$set('allocation_ports', $ports->all());
}

View File

@ -40,8 +40,8 @@ class NodeMemoryChart extends ChartWidget
/** @var Node $node */
$node = $this->record;
$total = $node->statistics()['memory_total'] ?? 0;
$used = $node->statistics()['memory_used'] ?? 0;
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
$unused = $total - $used;
return [

View File

@ -40,8 +40,8 @@ class NodeStorageChart extends ChartWidget
/** @var Node $node */
$node = $this->record;
$total = $node->statistics()['disk_total'] ?? 0;
$used = $node->statistics()['disk_used'] ?? 0;
$total = ($node->statistics()['disk_total'] ?? 0) / 1024 / 1024 / 1024;
$used = ($node->statistics()['disk_used'] ?? 0) / 1024 / 1024 / 1024;
$unused = $total - $used;
return [

View File

@ -10,6 +10,11 @@ class ServerResource extends Resource
{
protected static ?string $model = Server::class;
public static function getNavigationBadge(): ?string
{
return static::getModel()::count();
}
protected static ?string $navigationIcon = 'tabler-brand-docker';
protected static ?string $recordTitleAttribute = 'name';

View File

@ -487,11 +487,12 @@ class CreateServer extends CreateRecord
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MB')
->suffix('MiB')
->default(0)
->required()
->columnSpan(2)
->numeric(),
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
@ -517,11 +518,12 @@ class CreateServer extends CreateRecord
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix('MB')
->suffix('MiB')
->default(0)
->required()
->columnSpan(2)
->numeric(),
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
@ -551,7 +553,9 @@ class CreateServer extends CreateRecord
->default(0)
->required()
->columnSpan(2)
->numeric(),
->numeric()
->minValue(0)
->helperText('100% equals one logical thread'),
]),
Forms\Components\Grid::make()
@ -593,7 +597,7 @@ class CreateServer extends CreateRecord
})
->label('Swap Memory')
->default(0)
->suffix('MB')
->suffix('MiB')
->minValue(-1)
->columnSpan(2)
->inlineLabel()
@ -610,7 +614,7 @@ class CreateServer extends CreateRecord
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('oom_disabled')
Forms\Components\ToggleButtons::make('oom_killer')
->label('OOM Killer')
->inlineLabel()->inline()
->default(false)

View File

@ -244,35 +244,57 @@ class EditServer extends EditRecord
]))
->schema([
Forms\Components\Repeater::make('server_variables')
->label('')
->relationship('serverVariables')
->grid()
->deletable(false)
->addable(false)
->schema([
Forms\Components\TextInput::make('variable_value')
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
foreach ($data as $key => $value) {
if (!isset($data['variable_value'])) {
$data['variable_value'] = '';
}
}
return $data;
})
->reorderable(false)->addable(false)->deletable(false)
->schema(function () {
$text = Forms\Components\TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->maxLength(191)
->rules([
fn (ServerVariable $variable): Closure => function (string $attribute, $value, Closure $fail) use ($variable) {
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $variable->variable->rules,
'validatorkey' => $serverVariable->variable->rules,
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $variable->variable->name);
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
$fail($message);
}
},
])
->label(fn (ServerVariable $variable) => $variable->variable->name)
->hintIcon('tabler-code')
->hintIconTooltip(fn (ServerVariable $variable) => $variable->variable->rules)
->prefix(fn (ServerVariable $variable) => '{{' . $variable->variable->env_variable . '}}')
->helperText(fn (ServerVariable $variable) => $variable->variable->description ?: '—')
->maxLength(191),
]);
Forms\Components\Hidden::make('variable_id'),
])
$select = Forms\Components\Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
->options($this->getSelectOptionsFromRules(...))
->selectablePlaceholder(false);
$components = [$text, $select];
/** @var Forms\Components\Component $component */
foreach ($components as &$component) {
$component = $component
->live(onBlur: true)
->hintIcon('tabler-code')
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules)
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
}
return $components;
})
->columnSpan(2),
]),
@ -311,10 +333,11 @@ class EditServer extends EditRecord
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MB')
->suffix('MiB')
->required()
->columnSpan(2)
->numeric(),
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
@ -340,10 +363,11 @@ class EditServer extends EditRecord
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix('MB')
->suffix('MiB')
->required()
->columnSpan(2)
->numeric(),
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
@ -372,7 +396,8 @@ class EditServer extends EditRecord
->suffix('%')
->required()
->columnSpan(2)
->numeric(),
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
@ -417,7 +442,7 @@ class EditServer extends EditRecord
'limited', false => false,
})
->label('Swap Memory')->inlineLabel()
->suffix('MB')
->suffix('MiB')
->minValue(-1)
->columnSpan(2)
->required()
@ -432,7 +457,7 @@ class EditServer extends EditRecord
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('oom_disabled')
Forms\Components\ToggleButtons::make('oom_killer')
->label('OOM Killer')->inlineLabel()->inline()
->columnSpan(2)
->options([
@ -487,13 +512,6 @@ class EditServer extends EditRecord
->color('danger')
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
->requiresConfirmation(),
Actions\DeleteAction::make('Force Delete')
->label('Force Delete')
->hidden()
->successRedirectUrl(route('filament.admin.resources.servers.index'))
->color('danger')
->after(fn (Server $server) => resolve(ServerDeletionService::class)->withForce()->handle($server))
->requiresConfirmation(),
Actions\Action::make('console')
->label('Console')
->icon('tabler-terminal')
@ -520,4 +538,35 @@ class EditServer extends EditRecord
ServerResource\RelationManagers\AllocationsRelationManager::class,
];
}
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
{
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
);
if ($component instanceof Forms\Components\Select) {
return $containsRuleIn;
}
if ($component instanceof Forms\Components\TextInput) {
return !$containsRuleIn;
}
throw new \Exception('Component type not supported: ' . $component::class);
}
private function getSelectOptionsFromRules(Forms\Get $get): array
{
$inRule = str($get('rules'))->explode('|')->reduce(
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
);
return str($inRule)
->after('in:')
->explode(',')
->each(fn ($value) => str($value)->trim())
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
}

View File

@ -31,7 +31,7 @@ class AllocationsRelationManager extends RelationManager
// ->actions
// ->groups
->columns([
Tables\Columns\TextColumn::make('ip_alias')->label('Alias'),
Tables\Columns\TextInputColumn::make('ip_alias')->label('Alias'),
Tables\Columns\TextColumn::make('ip')->label('IP'),
Tables\Columns\TextColumn::make('port')->label('Port'),
Tables\Columns\IconColumn::make('primary')
@ -56,8 +56,8 @@ class AllocationsRelationManager extends RelationManager
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
])
->headerActions([
Tables\Actions\CreateAction::make()->label('Create Allocation'),
//Tables\Actions\AssociateAction::make()->label('Add Allocation'),
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
//TODO Tables\Actions\AssociateAction::make()->label('Add Allocation'),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([

View File

@ -11,6 +11,10 @@ class UserResource extends Resource
{
protected static ?string $model = User::class;
public static function getNavigationBadge(): ?string
{
return static::getModel()::count();
}
protected static ?string $navigationIcon = 'tabler-users';
protected static ?string $recordTitleAttribute = 'username';

View File

@ -67,6 +67,12 @@ class EditUser extends EditRecord
{
return [
Actions\DeleteAction::make(),
$this->getSaveFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -2,21 +2,12 @@
namespace App\Http\Controllers\Admin\Servers;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Models\Allocation;
use App\Models\Node;
use Carbon\CarbonImmutable;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Http\Request;
use App\Models\Server;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Http;
use Lcobucci\JWT\Token\Plain;
use Prologue\Alerts\AlertsMessageBag;
use App\Models\ServerTransfer;
use Illuminate\Database\ConnectionInterface;
use App\Http\Controllers\Controller;
use App\Services\Nodes\NodeJWTService;
use App\Models\Server;
use App\Services\Servers\TransferServerService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Prologue\Alerts\AlertsMessageBag;
class ServerTransferController extends Controller
{
@ -25,30 +16,10 @@ class ServerTransferController extends Controller
*/
public function __construct(
private AlertsMessageBag $alert,
private ConnectionInterface $connection,
private NodeJWTService $nodeJWTService,
private TransferServerService $transferServerService,
) {
}
private function notify(Server $server, Plain $token): void
{
try {
Http::daemon($server->node)->post('/api/transfer', [
'json' => [
'server_id' => $server->uuid,
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
'token' => 'Bearer ' . $token->toString(),
'server' => [
'uuid' => $server->uuid,
'start_on_completion' => false,
],
],
])->toPsrResponse();
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
/**
* Starts a transfer of a server to a new node.
*
@ -62,85 +33,12 @@ class ServerTransferController extends Controller
'allocation_additional' => 'nullable',
]);
$node_id = $validatedData['node_id'];
$allocation_id = intval($validatedData['allocation_id']);
$additional_allocations = array_map('intval', $validatedData['allocation_additional'] ?? []);
// Check if the node is viable for the transfer.
$node = Node::query()
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.id', $node_id)
->first();
if (!$node->isViable($server->memory, $server->disk)) {
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
}
$server->validateTransferState();
$this->connection->transaction(function () use ($server, $node_id, $allocation_id, $additional_allocations) {
// Create a new ServerTransfer entry.
$transfer = new ServerTransfer();
$transfer->server_id = $server->id;
$transfer->old_node = $server->node_id;
$transfer->new_node = $node_id;
$transfer->old_allocation = $server->allocation_id;
$transfer->new_allocation = $allocation_id;
$transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id')->all();
$transfer->new_additional_allocations = $additional_allocations;
$transfer->save();
// Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress.
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);
// Generate a token for the destination node that the source node can use to authenticate with.
$token = $this->nodeJWTService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setSubject($server->uuid)
->handle($transfer->newNode, $server->uuid, 'sha256');
// Notify the source node of the pending outgoing transfer.
$this->notify($server, $token);
return $transfer;
});
if ($this->transferServerService->handle($server, $validatedData)) {
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
} else {
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
}
return redirect()->route('admin.servers.view.manage', $server->id);
}
/**
* Assigns the specified allocations to the specified server.
*/
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
{
$allocations = $additional_allocations;
$allocations[] = $allocation_id;
$node = Node::query()->findOrFail($node_id);
$unassigned = $node->allocations()
->whereNull('server_id')
->pluck('id')
->toArray();
$updateIds = [];
foreach ($allocations as $allocation) {
if (!in_array($allocation, $unassigned)) {
continue;
}
$updateIds[] = $allocation;
}
if (!empty($updateIds)) {
Allocation::query()->whereIn('id', $updateIds)->update(['server_id' => $server->id]);
}
}
}

View File

@ -126,7 +126,7 @@ class ServersController extends Controller
$this->buildModificationService->handle($server, $request->only([
'allocation_id', 'add_allocations', 'remove_allocations',
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled',
'database_limit', 'allocation_limit', 'backup_limit', 'oom_killer',
]));
} catch (DataValidationException $exception) {
throw new ValidationException($exception->getValidator());

View File

@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers\Api\Application\DatabaseHosts;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use App\Models\DatabaseHost;
use Spatie\QueryBuilder\QueryBuilder;
use App\Services\Databases\Hosts\HostUpdateService;
use App\Services\Databases\Hosts\HostCreationService;
use App\Transformers\Api\Application\DatabaseHostTransformer;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\DatabaseHosts\GetDatabaseHostRequest;
use App\Http\Requests\Api\Application\DatabaseHosts\StoreDatabaseHostRequest;
use App\Http\Requests\Api\Application\DatabaseHosts\DeleteDatabaseHostRequest;
use App\Http\Requests\Api\Application\DatabaseHosts\UpdateDatabaseHostRequest;
class DatabaseHostController extends ApplicationApiController
{
/**
* DatabaseHostController constructor.
*/
public function __construct(
private HostCreationService $creationService,
private HostUpdateService $updateService
) {
parent::__construct();
}
/**
* Return all the database hosts currently registered on the Panel.
*/
public function index(GetDatabaseHostRequest $request): array
{
$databases = QueryBuilder::for(DatabaseHost::query())
->allowedFilters(['name', 'host'])
->allowedSorts(['id', 'name', 'host'])
->paginate($request->query('per_page') ?? 10);
return $this->fractal->collection($databases)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->toArray();
}
/**
* Return a single database host.
*/
public function view(GetDatabaseHostRequest $request, DatabaseHost $databaseHost): array
{
return $this->fractal->item($databaseHost)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->toArray();
}
/**
* Store a new database host on the Panel and return an HTTP/201 response code with the
* new database host attached.
*
* @throws \Throwable
*/
public function store(StoreDatabaseHostRequest $request): JsonResponse
{
$databaseHost = $this->creationService->handle($request->validated());
return $this->fractal->item($databaseHost)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->addMeta([
'resource' => route('api.application.databases.view', [
'database_host' => $databaseHost->id,
]),
])
->respond(201);
}
/**
* Update a database host on the Panel and return the updated record to the user.
*
* @throws \Throwable
*/
public function update(UpdateDatabaseHostRequest $request, DatabaseHost $databaseHost): array
{
$databaseHost = $this->updateService->handle($databaseHost->id, $request->validated());
return $this->fractal->item($databaseHost)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->toArray();
}
/**
* Delete a database host from the Panel.
*
* @throws \Exception
*/
public function delete(DeleteDatabaseHostRequest $request, DatabaseHost $databaseHost): Response
{
$databaseHost->delete();
return $this->returnNoContent();
}
}

View File

@ -2,12 +2,14 @@
namespace App\Http\Controllers\Api\Application\Servers;
use Illuminate\Http\Response;
use App\Models\Server;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\ReinstallServerService;
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Servers\ReinstallServerService;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\TransferServerService;
use Illuminate\Http\Response;
class ServerManagementController extends ApplicationApiController
{
@ -16,7 +18,9 @@ class ServerManagementController extends ApplicationApiController
*/
public function __construct(
private ReinstallServerService $reinstallServerService,
private SuspensionService $suspensionService
private SuspensionService $suspensionService,
private TransferServerService $transferServerService,
private DaemonServerRepository $daemonServerRepository,
) {
parent::__construct();
}
@ -57,4 +61,44 @@ class ServerManagementController extends ApplicationApiController
return $this->returnNoContent();
}
/**
* Starts a transfer of a server to a new node.
*/
public function startTransfer(ServerWriteRequest $request, Server $server): Response
{
$validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable',
]);
if ($this->transferServerService->handle($server, $validatedData)) {
// Transfer started
$this->returnNoContent();
} else {
// Node was not viable
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
}
}
/**
* Cancels a transfer of a server to a new node.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
*/
public function cancelTransfer(ServerWriteRequest $request, Server $server): Response
{
if (!$transfer = $server->transfer) {
// Server is not transferring
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
}
$transfer->successful = true;
$transfer->save();
$this->daemonServerRepository->setServer($server)->cancelTransfer();
return $this->returnNoContent();
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class DeleteDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected int $permission = AdminAcl::WRITE;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class GetDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected int $permission = AdminAcl::READ;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Models\DatabaseHost;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected int $permission = AdminAcl::WRITE;
public function rules(array $rules = null): array
{
return $rules ?? DatabaseHost::getRules();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Models\DatabaseHost;
class UpdateDatabaseHostRequest extends StoreDatabaseHostRequest
{
public function rules(array $rules = null): array
{
/** @var DatabaseHost $databaseHost */
$databaseHost = $this->route()->parameter('database_host');
return $rules ?? DatabaseHost::getRulesForUpdate($databaseHost->id);
}
}

View File

@ -32,7 +32,7 @@ class StoreServerRequest extends ApplicationApiRequest
'startup' => $rules['startup'],
'environment' => 'present|array',
'skip_scripts' => 'sometimes|boolean',
'oom_disabled' => 'sometimes|boolean',
'oom_killer' => 'sometimes|boolean',
// Resource limitations
'limits' => 'required|array',
@ -94,7 +94,7 @@ class StoreServerRequest extends ApplicationApiRequest
'database_limit' => array_get($data, 'feature_limits.databases'),
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
'backup_limit' => array_get($data, 'feature_limits.backups'),
'oom_disabled' => array_get($data, 'oom_disabled'),
'oom_killer' => array_get($data, 'oom_killer'),
];
}

View File

@ -16,7 +16,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
return [
'allocation' => $rules['allocation_id'],
'oom_disabled' => $rules['oom_disabled'],
'oom_killer' => $rules['oom_killer'],
'limits' => 'sometimes|array',
'limits.memory' => $this->requiredToOptional('memory', $rules['memory'], true),

View File

@ -19,9 +19,11 @@ class StoreScheduleRequest extends ViewScheduleRequest
return [
'name' => $rules['name'],
'is_active' => array_merge(['filled'], $rules['is_active']),
'only_when_online' => $rules['only_when_online'],
'minute' => $rules['cron_minute'],
'hour' => $rules['cron_hour'],
'day_of_month' => $rules['cron_day_of_month'],
'month' => $rules['cron_month'],
'day_of_week' => $rules['cron_day_of_week'],
];
}

View File

@ -35,7 +35,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @property int $io
* @property int $cpu
* @property string|null $threads
* @property bool $oom_disabled
* @property bool $oom_killer
* @property int $allocation_id
* @property int $egg_id
* @property string $startup
@ -90,7 +90,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @method static \Illuminate\Database\Eloquent\Builder|Server whereMemory($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereNodeId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomDisabled($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomKiller($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOwnerId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereSkipScripts($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereStartup($value)
@ -124,7 +124,7 @@ class Server extends Model
*/
protected $attributes = [
'status' => ServerState::Installing,
'oom_disabled' => true,
'oom_killer' => false,
'installed_at' => null,
];
@ -150,7 +150,7 @@ class Server extends Model
'io' => 'required|numeric|between:0,1000',
'cpu' => 'required|numeric|min:0',
'threads' => 'nullable|regex:/^[0-9-,]+$/',
'oom_disabled' => 'sometimes|boolean',
'oom_killer' => 'sometimes|boolean',
'disk' => 'required|numeric|min:0',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'egg_id' => 'required|exists:eggs,id',
@ -174,7 +174,7 @@ class Server extends Model
'disk' => 'integer',
'io' => 'integer',
'cpu' => 'integer',
'oom_disabled' => 'boolean',
'oom_killer' => 'boolean',
'allocation_id' => 'integer',
'egg_id' => 'integer',
'database_limit' => 'integer',

View File

@ -19,6 +19,7 @@ use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Saade\FilamentLaravelLog\FilamentLaravelLogPlugin;
class AdminPanelProvider extends PanelProvider
{
@ -35,8 +36,8 @@ class AdminPanelProvider extends PanelProvider
->default()
->id('admin')
->path('admin')
->topNavigation(true)
->login()
->brandName('Pelican')
->homeUrl('/')
->favicon('/pelican.ico')
->profile(EditProfile::class, false)
@ -74,6 +75,13 @@ class AdminPanelProvider extends PanelProvider
])
->authMiddleware([
Authenticate::class,
]);
])
->plugin(
FilamentLaravelLogPlugin::make()
->navigationLabel('Logs')
->navigationIcon('tabler-file-info')
->slug('logs')
->authorize(fn () => auth()->user()->root_admin)
);
}
}

View File

@ -141,6 +141,46 @@ class DaemonServerRepository extends DaemonRepository
}
}
/**
* Cancels a server transfer.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
*/
public function cancelTransfer(): void
{
Assert::isInstanceOf($this->server, Server::class);
if ($transfer = $this->server->transfer) {
// Source node
$this->setNode($transfer->oldNode);
try {
$this->getHttpClient()->delete(sprintf(
'/api/servers/%s/transfer',
$this->server->uuid
));
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
// Destination node
$this->setNode($transfer->newNode);
try {
$this->getHttpClient()->delete('/api/transfer', [
'json' => [
'server_id' => $this->server->uuid,
'server' => [
'uuid' => $this->server->uuid,
],
],
]);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
}
/**
* Revokes a single user's JTI by using their ID. This is simply a helper function to
* make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted

View File

@ -40,8 +40,12 @@ class BuildModificationService
throw_unless($existingAllocation, new DisplayException('The requested default allocation is not currently assigned to this server.'));
}
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
$data['oom_killer'] = !$data['oom_disabled'];
}
// If any of these values are passed through in the data array go ahead and set them correctly on the server model.
$merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
$merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
$server->forceFill(array_merge($merge, [
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,

View File

@ -59,14 +59,12 @@ class ServerConfigurationStructureService
'cpu_limit' => $server->cpu,
'threads' => $server->threads,
'disk_space' => $server->disk,
'oom_disabled' => $server->oom_disabled,
// This field is deprecated — use "oom_killer".
'oom_disabled' => !$server->oom_killer,
'oom_killer' => $server->oom_killer,
],
'container' => [
'image' => $server->image,
// This field is deprecated — use the value in the "build" block.
//
// TODO: remove this key in V2.
'oom_disabled' => $server->oom_disabled,
'requires_rebuild' => false,
],
'allocations' => [
@ -110,7 +108,7 @@ class ServerConfigurationStructureService
return $item->pluck('port');
})->toArray(),
'env' => $this->environment->handle($server),
'oom_disabled' => $server->oom_disabled,
'oom_disabled' => !$server->oom_killer,
'memory' => (int) $server->memory,
'swap' => (int) $server->swap,
'io' => (int) $server->io,

View File

@ -47,6 +47,10 @@ class ServerCreationService
*/
public function handle(array $data, DeploymentObject $deployment = null): Server
{
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
$data['oom_killer'] = !$data['oom_disabled'];
}
// If a deployment object has been passed we need to get the allocation
// that the server should use, and assign the node from that allocation.
if ($deployment instanceof DeploymentObject) {
@ -142,7 +146,7 @@ class ServerCreationService
'io' => Arr::get($data, 'io'),
'cpu' => Arr::get($data, 'cpu'),
'threads' => Arr::get($data, 'threads'),
'oom_disabled' => Arr::get($data, 'oom_disabled') ?? true,
'oom_killer' => Arr::get($data, 'oom_killer') ?? false,
'allocation_id' => Arr::get($data, 'allocation_id'),
'egg_id' => Arr::get($data, 'egg_id'),
'startup' => Arr::get($data, 'startup'),

View File

@ -0,0 +1,131 @@
<?php
namespace App\Services\Servers;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Models\Allocation;
use App\Models\Node;
use App\Models\Server;
use App\Models\ServerTransfer;
use App\Services\Nodes\NodeJWTService;
use Carbon\CarbonImmutable;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Facades\Http;
use Lcobucci\JWT\Token\Plain;
class TransferServerService
{
/**
* TransferService constructor.
*/
public function __construct(
private ConnectionInterface $connection,
private NodeJWTService $nodeJWTService,
) {
}
private function notify(Server $server, Plain $token): void
{
try {
Http::daemon($server->node)->post('/api/transfer', [
'json' => [
'server_id' => $server->uuid,
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
'token' => 'Bearer ' . $token->toString(),
'server' => [
'uuid' => $server->uuid,
'start_on_completion' => false,
],
],
])->toPsrResponse();
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
/**
* Starts a transfer of a server to a new node.
*
* @throws \Throwable
*/
public function handle(Server $server, array $data): bool
{
$node_id = $data['node_id'];
$allocation_id = intval($data['allocation_id']);
$additional_allocations = array_map('intval', $data['allocation_additional'] ?? []);
// Check if the node is viable for the transfer.
$node = Node::query()
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.id', $node_id)
->first();
if (!$node->isViable($server->memory, $server->disk)) {
return false;
}
$server->validateTransferState();
$this->connection->transaction(function () use ($server, $node_id, $allocation_id, $additional_allocations) {
// Create a new ServerTransfer entry.
$transfer = new ServerTransfer();
$transfer->server_id = $server->id;
$transfer->old_node = $server->node_id;
$transfer->new_node = $node_id;
$transfer->old_allocation = $server->allocation_id;
$transfer->new_allocation = $allocation_id;
$transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id')->all();
$transfer->new_additional_allocations = $additional_allocations;
$transfer->save();
// Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress.
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);
// Generate a token for the destination node that the source node can use to authenticate with.
$token = $this->nodeJWTService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setSubject($server->uuid)
->handle($transfer->newNode, $server->uuid, 'sha256');
// Notify the source node of the pending outgoing transfer.
$this->notify($server, $token);
return $transfer;
});
return true;
}
/**
* Assigns the specified allocations to the specified server.
*/
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
{
$allocations = $additional_allocations;
$allocations[] = $allocation_id;
$node = Node::query()->findOrFail($node_id);
$unassigned = $node->allocations()
->whereNull('server_id')
->pluck('id')
->toArray();
$updateIds = [];
foreach ($allocations as $allocation) {
if (!in_array($allocation, $unassigned)) {
continue;
}
$updateIds[] = $allocation;
}
if (!empty($updateIds)) {
Allocation::query()->whereIn('id', $updateIds)->update(['server_id' => $server->id]);
}
}
}

View File

@ -2,8 +2,10 @@
namespace App\Transformers\Api\Application;
use App\Models\Node;
use App\Models\Database;
use App\Models\DatabaseHost;
use League\Fractal\Resource\Item;
use League\Fractal\Resource\Collection;
use League\Fractal\Resource\NullResource;
use App\Services\Acl\Api\AdminAcl;
@ -12,6 +14,7 @@ class DatabaseHostTransformer extends BaseTransformer
{
protected array $availableIncludes = [
'databases',
'node',
];
/**
@ -54,4 +57,20 @@ class DatabaseHostTransformer extends BaseTransformer
return $this->collection($model->getRelation('databases'), $this->makeTransformer(ServerDatabaseTransformer::class), Database::RESOURCE_NAME);
}
/**
* Include the node associated with this host.
*
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includeNode(DatabaseHost $model): Item|NullResource
{
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) {
return $this->null();
}
$model->loadMissing('node');
return $this->item($model->getRelation('node'), $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME);
}
}

View File

@ -65,7 +65,9 @@ class ServerTransformer extends BaseTransformer
'io' => $server->io,
'cpu' => $server->cpu,
'threads' => $server->threads,
'oom_disabled' => $server->oom_disabled,
// This field is deprecated, please use "oom_killer".
'oom_disabled' => !$server->oom_killer,
'oom_killer' => $server->oom_killer,
],
'feature_limits' => [
'databases' => $server->database_limit,

View File

@ -56,7 +56,9 @@ class ServerTransformer extends BaseClientTransformer
'io' => $server->io,
'cpu' => $server->cpu,
'threads' => $server->threads,
'oom_disabled' => $server->oom_disabled,
// This field is deprecated, please use "oom_killer".
'oom_disabled' => !$server->oom_killer,
'oom_killer' => $server->oom_killer,
],
'invocation' => $service->handle($server, !$user->can(Permission::ACTION_STARTUP_READ, $server)),
'docker_image' => $server->image,

View File

@ -32,6 +32,7 @@
"prologue/alerts": "^1.2",
"ryangjchandler/blade-tabler-icons": "^2.3",
"s1lentium/iptools": "~1.2.0",
"saade/filament-laravel-log": "^3.2",
"spatie/laravel-fractal": "^6.1",
"spatie/laravel-query-builder": "^5.8",
"symfony/mailgun-mailer": "^7.0",

761
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ use Illuminate\Support\Facades\Facade;
return [
'name' => 'Pelican',
'name' => env('APP_NAME', 'Pelican'),
'version' => 'canary',

View File

@ -46,6 +46,8 @@ return [
],
'redis' => [
'client' => env('REDIS_CLIENT', 'predis'),
'default' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),
'path' => env('REDIS_PATH', '/run/redis/redis.sock'),

View File

@ -35,7 +35,7 @@ class ServerFactory extends Factory
'io' => 500,
'cpu' => 0,
'threads' => null,
'oom_disabled' => 0,
'oom_killer' => false,
'startup' => '/bin/bash echo "hello world"',
'image' => 'foo/bar:latest',
'allocation_limit' => null,

View File

@ -0,0 +1,45 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->tinyInteger('oom_killer')->unsigned()->default(0)->after('oom_disabled');
});
DB::table('servers')->select(['id', 'oom_disabled'])->cursor()->each(function ($server) {
DB::table('servers')->where('id', $server->id)->update(['oom_killer' => !$server->oom_disabled]);
});
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('oom_disabled');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->tinyInteger('oom_disabled')->unsigned()->default(0)->after('oom_killer');
});
DB::table('servers')->select(['id', 'oom_killer'])->cursor()->each(function ($server) {
DB::table('servers')->where('id', $server->id)->update(['oom_disabled' => !$server->oom_killer]);
});
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('oom_killer');
});
}
};

View File

@ -495,7 +495,7 @@ CREATE TABLE `servers` (
`io` int unsigned NOT NULL,
`cpu` int unsigned NOT NULL,
`threads` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`oom_disabled` tinyint unsigned NOT NULL DEFAULT '0',
`oom_killer` tinyint unsigned NOT NULL DEFAULT '0',
`allocation_id` int unsigned NOT NULL,
`egg_id` int unsigned NOT NULL,
`startup` text COLLATE utf8mb4_unicode_ci NOT NULL,
@ -844,3 +844,4 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (197,'2024_03_12_15
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (198,'2024_03_14_055537_remove_locations_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (201,'2024_04_20_214441_add_egg_var_sort',3);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (203,'2024_04_14_002250_update_column_names',4);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (204,'2024_05_08_094823_rename_oom_disabled_column_to_oom_killer',1);

View File

@ -17,7 +17,6 @@
<env name="MAIL_MAILER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="MAIL_DRIVER" value="array"/>
</php>
<source>
<include>

View File

@ -0,0 +1 @@
.mt-4{margin-top:1rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}html.dark .ace-filament .ace_scrollbar::-webkit-scrollbar{width:.75rem}html.dark .ace-filament .ace_scrollbar::-webkit-scrollbar-track{--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity))}html.dark .ace-filament .ace_scrollbar::-webkit-scrollbar-thumb{border-radius:4px;--tw-bg-opacity:1;background-color:rgba(var(--gray-800),var(--tw-bg-opacity))}html.dark .ace-filament .ace_gutter{--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}html.dark .ace-filament .ace_print-margin{display:none}html.dark .ace-filament{--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--gray-200),var(--tw-text-opacity));--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-color:#fff3}html.dark .ace-filament,html:not(.dark) .ace-filament{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}html:not(.dark) .ace-filament{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-color:rgba(var(--gray-950),0.1)}html.dark .ace-filament .ace_cursor{--tw-text-opacity:1;color:rgba(var(--gray-200),var(--tw-text-opacity))}html.dark .ace-filament .ace_marker-layer .ace_selection{background-color:rgba(var(--primary-500),.75)}html.dark .ace-filament.ace_multiselect .ace_selection.ace_start{box-shadow:0 0 3px 0 #002240}html.dark .ace-filament .ace_marker-layer .ace_step{background:#7f6f13}html.dark .ace-filament .ace_marker-layer .ace_bracket{margin:-1px 0 0 -1px;border:1px solid #ffffff26}html.dark .ace-filament .ace_marker-layer .ace_active-line{background:#18b69b1a}html.dark .ace-filament .ace_gutter-active-line{background-color:#00000059}html.dark .ace-filament .ace_marker-layer .ace_selected-word{border:1px solid #b36539bf}html.dark .ace-filament .ace_invisible{color:#ffffff26}html.dark .ace-filament .ace_keyword,html.dark .ace-filament .ace_meta{color:#ff9d00}html.dark .ace-filament .ace_constant,html.dark .ace-filament .ace_constant.ace_character,html.dark .ace-filament .ace_constant.ace_character.ace_escape,html.dark .ace-filament .ace_constant.ace_other{color:#ff628c}html.dark .ace-filament .ace_invalid{color:#f8f8f8;background-color:#800f00}html.dark .ace-filament .ace_support{color:#80ffbb}html.dark .ace-filament .ace_support.ace_constant{color:#eb939a}html.dark .ace-filament .ace_fold{background-color:#ff9d00;border-color:#f9fafb}html.dark .ace-filament .ace_support.ace_function{color:#ffb054}html.dark .ace-filament .ace_storage{color:#ffee80}html.dark .ace-filament .ace_entity{color:#fd0}html.dark .ace-filament .ace_string{color:#7cd827}html.dark .ace-filament .ace_string.ace_regexp{color:#80ffc2}html.dark .ace-filament .ace_comment{font-style:italic;color:#6b7280}html.dark .ace-filament .ace_heading,html.dark .ace-filament .ace_markup.ace_heading{color:#c8e4fd;background-color:#001221}html.dark .ace-filament .ace_list,html.dark .ace-filament .ace_markup.ace_list{background-color:#130d26}html.dark .ace-filament .ace_variable{color:#ccc}html.dark .ace-filament .ace_variable.ace_language{color:#ff80e1}html.dark .ace-filament .ace_meta.ace_tag{color:#9effff}html.dark .ace-filament .ace_indent-guide{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHCLSvkPAAP3AgSDTRd4AAAAAElFTkSuQmCC) 100% repeat-y}

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
![image](https://github.com/pelican-dev/panel/assets/1296882/6c545ae3-24b9-4d30-af21-d08d2e53b673)
<img width="20%" src="https://raw.githubusercontent.com/pelican-dev/panel/main/public/pelican.svg" alt="logo">
# Pelican Panel

View File

@ -203,8 +203,8 @@
</div>
<div class="form-group col-xs-12">
<div class="checkbox checkbox-primary no-margin-bottom">
<input type="checkbox" id="pOomDisabled" name="oom_disabled" value="0" {{ \App\Helpers\Utilities::checked('oom_disabled', 0) }} />
<label for="pOomDisabled" class="strong">Enable OOM Killer</label>
<input type="checkbox" id="pOomKiller" name="oom_killer" value="0" {{ \App\Helpers\Utilities::checked('oom_killer', 0) }} />
<label for="pOomKiller" class="strong">Enable OOM Killer</label>
</div>
<p class="small text-muted no-margin">Terminates the server if it breaches the memory limits. Enabling OOM killer may cause server processes to exit unexpectedly.</p>

View File

@ -74,11 +74,11 @@
<label for="cpu" class="control-label">OOM Killer</label>
<div>
<div class="radio radio-danger radio-inline">
<input type="radio" id="pOomKillerEnabled" value="0" name="oom_disabled" @if(!$server->oom_disabled)checked @endif>
<input type="radio" id="pOomKillerEnabled" value="1" name="oom_killer" @if(!$server->oom_killer)checked @endif>
<label for="pOomKillerEnabled">Enabled</label>
</div>
<div class="radio radio-success radio-inline">
<input type="radio" id="pOomKillerDisabled" value="1" name="oom_disabled" @if($server->oom_disabled)checked @endif>
<input type="radio" id="pOomKillerDisabled" value="0" name="oom_killer" @if($server->oom_killer)checked @endif>
<label for="pOomKillerDisabled">Disabled</label>
</div>
<p class="text-muted small">

View File

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" data-theme="{{ config('scramble.theme', 'dark') }}">
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

View File

@ -69,6 +69,8 @@ Route::prefix('/servers')->group(function () {
Route::post('/{server:id}/suspend', [Application\Servers\ServerManagementController::class, 'suspend'])->name('api.application.servers.suspend');
Route::post('/{server:id}/unsuspend', [Application\Servers\ServerManagementController::class, 'unsuspend'])->name('api.application.servers.unsuspend');
Route::post('/{server:id}/reinstall', [Application\Servers\ServerManagementController::class, 'reinstall'])->name('api.application.servers.reinstall');
Route::post('/{server:id}/transfer', [Application\Servers\ServerManagementController::class, 'startTransfer'])->name('api.application.servers.transfer');
Route::post('/{server:id}/transfer/cancel', [Application\Servers\ServerManagementController::class, 'cancelTransfer'])->name('api.application.servers.transfer.cancel');
Route::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']);
Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']);
@ -97,3 +99,22 @@ Route::prefix('/eggs')->group(function () {
Route::get('/', [Application\Eggs\EggController::class, 'index'])->name('api.application.eggs.eggs');
Route::get('/{egg:id}', [Application\Eggs\EggController::class, 'view'])->name('api.application.eggs.eggs.view');
});
/*
|--------------------------------------------------------------------------
| Database Host Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /api/application/database-hosts
|
*/
Route::group(['prefix' => '/database-hosts'], function () {
Route::get('/', [Application\DatabaseHosts\DatabaseHostController::class, 'index'])->name('api.application.databasehosts');
Route::get('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'view'])->name('api.application.databasehosts.view');
Route::post('/', [Application\DatabaseHosts\DatabaseHostController::class, 'store']);
Route::patch('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'update']);
Route::delete('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'delete']);
});

View File

@ -57,7 +57,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase
$response = $this->actingAs($user)->postJson("/api/client/servers/$server->uuid/schedules", []);
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
foreach (['name', 'minute', 'hour', 'day_of_month', 'day_of_week'] as $i => $field) {
foreach (['name', 'minute', 'hour', 'day_of_month', 'month', 'day_of_week'] as $i => $field) {
$response->assertJsonPath("errors.$i.code", 'ValidationException');
$response->assertJsonPath("errors.$i.meta.rule", 'required');
$response->assertJsonPath("errors.$i.meta.source_field", $field);
@ -67,6 +67,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase
->postJson("/api/client/servers/$server->uuid/schedules", [
'name' => 'Testing',
'is_active' => 'no',
'only_when_online' => 'false',
'minute' => '*',
'hour' => '*',
'day_of_month' => '*',

View File

@ -114,7 +114,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
$this->daemonServerRepository->expects('sync')->withNoArgs()->andReturnUndefined();
$response = $this->getService()->handle($server, [
'oom_disabled' => false,
'oom_killer' => false,
'memory' => 256,
'swap' => 128,
'io' => 600,
@ -126,7 +126,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
'allocation_limit' => 20,
]);
$this->assertFalse($response->oom_disabled);
$this->assertFalse($response->oom_killer);
$this->assertSame(256, $response->memory);
$this->assertSame(128, $response->swap);
$this->assertSame(600, $response->io);

View File

@ -138,7 +138,7 @@ class ServerCreationServiceTest extends IntegrationTestCase
$this->assertSame($allocations[4]->id, $response->allocations[1]->id);
$this->assertFalse($response->isSuspended());
$this->assertTrue($response->oom_disabled);
$this->assertFalse($response->oom_killer);
$this->assertSame(0, $response->database_limit);
$this->assertSame(0, $response->allocation_limit);
$this->assertSame(0, $response->backup_limit);