mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-29 13:14:45 +02:00
Merge branch 'main' of github.com:pelican-dev/panel
This commit is contained in:
commit
b513366f35
@ -13,12 +13,8 @@ LOG_LEVEL=debug
|
|||||||
|
|
||||||
DB_CONNECTION=sqlite
|
DB_CONNECTION=sqlite
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
CACHE_STORE=file
|
CACHE_STORE=file
|
||||||
QUEUE_CONNECTION=sync
|
QUEUE_CONNECTION=database
|
||||||
SESSION_DRIVER=file
|
SESSION_DRIVER=file
|
||||||
|
|
||||||
HASHIDS_SALT=
|
HASHIDS_SALT=
|
||||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1,2 +1,2 @@
|
|||||||
github: pelican-dev
|
github: pelican-dev
|
||||||
custom: [https://buy.stripe.com/14kdU99SI4UT7ni9AB, https://buy.stripe.com/14kaHXc0Q9b9372eUU]
|
open_collective: pelican-panel
|
||||||
|
@ -11,23 +11,20 @@ class AppSettingsCommand extends Command
|
|||||||
{
|
{
|
||||||
use EnvironmentWriterTrait;
|
use EnvironmentWriterTrait;
|
||||||
public const CACHE_DRIVERS = [
|
public const CACHE_DRIVERS = [
|
||||||
'redis' => 'Redis',
|
|
||||||
'memcached' => 'Memcached',
|
|
||||||
'file' => 'Filesystem (recommended)',
|
'file' => 'Filesystem (recommended)',
|
||||||
|
'redis' => 'Redis',
|
||||||
];
|
];
|
||||||
|
|
||||||
public const SESSION_DRIVERS = [
|
public const SESSION_DRIVERS = [
|
||||||
'redis' => 'Redis',
|
|
||||||
'memcached' => 'Memcached',
|
|
||||||
'database' => 'MySQL Database',
|
|
||||||
'file' => 'Filesystem (recommended)',
|
'file' => 'Filesystem (recommended)',
|
||||||
|
'redis' => 'Redis',
|
||||||
|
'database' => 'MySQL Database',
|
||||||
'cookie' => 'Cookie',
|
'cookie' => 'Cookie',
|
||||||
];
|
];
|
||||||
|
|
||||||
public const QUEUE_DRIVERS = [
|
public const QUEUE_DRIVERS = [
|
||||||
|
'database' => 'MySQL Database (recommended)',
|
||||||
'redis' => 'Redis',
|
'redis' => 'Redis',
|
||||||
'database' => 'MySQL Database',
|
|
||||||
'sync' => 'Sync (recommended)',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $description = 'Configure basic environment settings for the Panel.';
|
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
|
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(
|
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
|
||||||
'Queue Driver',
|
'Queue Driver',
|
||||||
self::QUEUE_DRIVERS,
|
self::QUEUE_DRIVERS,
|
||||||
|
@ -31,7 +31,7 @@ class EmailSettingsCommand extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle(): void
|
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'),
|
trans('command/messages.environment.mail.ask_driver'),
|
||||||
[
|
[
|
||||||
'log' => 'Log',
|
'log' => 'Log',
|
||||||
@ -41,10 +41,10 @@ class EmailSettingsCommand extends Command
|
|||||||
'mandrill' => 'Mandrill',
|
'mandrill' => 'Mandrill',
|
||||||
'postmark' => 'Postmark',
|
'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)) {
|
if (method_exists($this, $method)) {
|
||||||
$this->{$method}();
|
$this->{$method}();
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,7 @@ use Filament\Notifications\Notification;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Prologue\Alerts\AlertsMessageBag;
|
use Prologue\Alerts\AlertsMessageBag;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||||
|
|
||||||
|
@ -13,6 +13,10 @@ class ApiKeyResource extends Resource
|
|||||||
protected static ?string $model = ApiKey::class;
|
protected static ?string $model = ApiKey::class;
|
||||||
protected static ?string $label = 'API Key';
|
protected static ?string $label = 'API Key';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count();
|
||||||
|
}
|
||||||
protected static ?string $navigationIcon = 'tabler-key';
|
protected static ?string $navigationIcon = 'tabler-key';
|
||||||
|
|
||||||
public static function canEdit($record): bool
|
public static function canEdit($record): bool
|
||||||
|
@ -10,6 +10,11 @@ class DatabaseHostResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = DatabaseHost::class;
|
protected static ?string $model = DatabaseHost::class;
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count();
|
||||||
|
}
|
||||||
|
|
||||||
protected static ?string $label = 'Databases';
|
protected static ?string $label = 'Databases';
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'tabler-database';
|
protected static ?string $navigationIcon = 'tabler-database';
|
||||||
|
@ -22,50 +22,59 @@ class CreateDatabaseHost extends CreateRecord
|
|||||||
{
|
{
|
||||||
return $form
|
return $form
|
||||||
->schema([
|
->schema([
|
||||||
Section::make()->schema([
|
Section::make()
|
||||||
Forms\Components\TextInput::make('host')
|
->columns([
|
||||||
->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.')
|
'default' => 2,
|
||||||
->required()
|
'sm' => 3,
|
||||||
->live()
|
'md' => 3,
|
||||||
->debounce(500)
|
'lg' => 4,
|
||||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
])
|
||||||
->maxLength(191),
|
->schema([
|
||||||
Forms\Components\TextInput::make('port')
|
Forms\Components\TextInput::make('host')
|
||||||
->helperText('The port that MySQL is running on for this host.')
|
->columnSpan(2)
|
||||||
->required()
|
->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.')
|
||||||
->numeric()
|
->required()
|
||||||
->default(3306)
|
->live(onBlur: true)
|
||||||
->minValue(0)
|
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||||
->maxValue(65535),
|
->maxLength(191),
|
||||||
Forms\Components\TextInput::make('username')
|
Forms\Components\TextInput::make('port')
|
||||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
->columnSpan(1)
|
||||||
->required()
|
->helperText('The port that MySQL is running on for this host.')
|
||||||
->maxLength(191),
|
->required()
|
||||||
Forms\Components\TextInput::make('password')
|
->numeric()
|
||||||
->helperText('The password for the database user.')
|
->default(3306)
|
||||||
->password()
|
->minValue(0)
|
||||||
->revealable()
|
->maxValue(65535),
|
||||||
->maxLength(191)
|
Forms\Components\TextInput::make('max_databases')
|
||||||
->required(),
|
->label('Max databases')
|
||||||
Forms\Components\TextInput::make('name')
|
->helpertext('Blank is unlimited.')
|
||||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
->numeric(),
|
||||||
->required()
|
Forms\Components\TextInput::make('name')
|
||||||
->maxLength(60),
|
->label('Display Name')
|
||||||
Forms\Components\Select::make('node_id')
|
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||||
->searchable()
|
->required()
|
||||||
->preload()
|
->maxLength(60),
|
||||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
Forms\Components\TextInput::make('username')
|
||||||
->label('Linked Node')
|
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||||
->relationship('node', 'name'),
|
->required()
|
||||||
])->columns([
|
->maxLength(191),
|
||||||
'default' => 1,
|
Forms\Components\TextInput::make('password')
|
||||||
'lg' => 2,
|
->helperText('The password for the database user.')
|
||||||
]),
|
->password()
|
||||||
|
->revealable()
|
||||||
|
->maxLength(191)
|
||||||
|
->required(),
|
||||||
|
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'),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function mutateFormDataBeforeSave(array $data): array
|
protected function mutateFormDataBeforeCreate(array $data): array
|
||||||
{
|
{
|
||||||
if (isset($data['password'])) {
|
if (isset($data['password'])) {
|
||||||
$data['password'] = encrypt($data['password']);
|
$data['password'] = encrypt($data['password']);
|
||||||
@ -73,4 +82,17 @@ class CreateDatabaseHost extends CreateRecord
|
|||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$this->getCreateFormAction()->formId('form'),
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,45 +17,54 @@ class EditDatabaseHost extends EditRecord
|
|||||||
{
|
{
|
||||||
return $form
|
return $form
|
||||||
->schema([
|
->schema([
|
||||||
Section::make()->schema([
|
Section::make()
|
||||||
Forms\Components\TextInput::make('host')
|
->columns([
|
||||||
->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.')
|
'default' => 2,
|
||||||
->required()
|
'sm' => 3,
|
||||||
->live()
|
'md' => 3,
|
||||||
->debounce(500)
|
'lg' => 4,
|
||||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
])
|
||||||
->maxLength(191),
|
->schema([
|
||||||
Forms\Components\TextInput::make('port')
|
Forms\Components\TextInput::make('host')
|
||||||
->helperText('The port that MySQL is running on for this host.')
|
->columnSpan(2)
|
||||||
->required()
|
->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.')
|
||||||
->numeric()
|
->required()
|
||||||
->default(3306)
|
->live(onBlur: true)
|
||||||
->minValue(0)
|
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||||
->maxValue(65535),
|
->maxLength(191),
|
||||||
Forms\Components\TextInput::make('username')
|
Forms\Components\TextInput::make('port')
|
||||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
->columnSpan(1)
|
||||||
->required()
|
->helperText('The port that MySQL is running on for this host.')
|
||||||
->maxLength(191),
|
->required()
|
||||||
Forms\Components\TextInput::make('password')
|
->numeric()
|
||||||
->helperText('The password for the database user.')
|
->minValue(0)
|
||||||
->password()
|
->maxValue(65535),
|
||||||
->revealable()
|
Forms\Components\TextInput::make('max_databases')
|
||||||
->maxLength(191)
|
->label('Max databases')
|
||||||
->required(),
|
->helpertext('Blank is unlimited.')
|
||||||
Forms\Components\TextInput::make('name')
|
->numeric(),
|
||||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
Forms\Components\TextInput::make('name')
|
||||||
->required()
|
->label('Display Name')
|
||||||
->maxLength(60),
|
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||||
Forms\Components\Select::make('node_id')
|
->required()
|
||||||
->searchable()
|
->maxLength(60),
|
||||||
->preload()
|
Forms\Components\TextInput::make('username')
|
||||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||||
->label('Linked Node')
|
->required()
|
||||||
->relationship('node', 'name'),
|
->maxLength(191),
|
||||||
])->columns([
|
Forms\Components\TextInput::make('password')
|
||||||
'default' => 1,
|
->helperText('The password for the database user.')
|
||||||
'lg' => 2,
|
->password()
|
||||||
]),
|
->revealable()
|
||||||
|
->maxLength(191)
|
||||||
|
->required(),
|
||||||
|
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'),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +72,7 @@ class EditDatabaseHost extends EditRecord
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make(),
|
Actions\DeleteAction::make(),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,4 +84,16 @@ class EditDatabaseHost extends EditRecord
|
|||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRelationManagers(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DatabaseHostResource\RelationManagers\DatabasesRelationManager::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ class ListDatabaseHosts extends ListRecords
|
|||||||
{
|
{
|
||||||
protected static string $resource = DatabaseHostResource::class;
|
protected static string $resource = DatabaseHostResource::class;
|
||||||
|
|
||||||
|
protected ?string $heading = 'Database Hosts';
|
||||||
|
|
||||||
public function table(Table $table): Table
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
@ -48,7 +50,7 @@ class ListDatabaseHosts extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make(),
|
Actions\CreateAction::make('create')->label('New Database Host'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,11 @@ class DatabaseResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Database::class;
|
protected static ?string $model = Database::class;
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count();
|
||||||
|
}
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'tabler-database';
|
protected static ?string $navigationIcon = 'tabler-database';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
@ -10,6 +10,10 @@ class EggResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Egg::class;
|
protected static ?string $model = Egg::class;
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count();
|
||||||
|
}
|
||||||
protected static ?string $navigationIcon = 'tabler-eggs';
|
protected static ?string $navigationIcon = 'tabler-eggs';
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'name';
|
protected static ?string $recordTitleAttribute = 'name';
|
||||||
|
@ -25,12 +25,16 @@ class EditEgg extends EditRecord
|
|||||||
Forms\Components\TextInput::make('name')
|
Forms\Components\TextInput::make('name')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(191)
|
->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.'),
|
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||||
Forms\Components\TextInput::make('uuid')
|
Forms\Components\TextInput::make('uuid')
|
||||||
|
->label('Egg UUID')
|
||||||
->disabled()
|
->disabled()
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
->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.'),
|
->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')
|
Forms\Components\Textarea::make('description')
|
||||||
->rows(3)
|
->rows(3)
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||||
@ -203,6 +207,19 @@ class EditEgg extends EditRecord
|
|||||||
->color('primary')
|
->color('primary')
|
||||||
// TODO uses old admin panel export service
|
// TODO uses old admin panel export service
|
||||||
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])),
|
->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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,11 @@ class MountResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Mount::class;
|
protected static ?string $model = Mount::class;
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count();
|
||||||
|
}
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'tabler-layers-linked';
|
protected static ?string $navigationIcon = 'tabler-layers-linked';
|
||||||
|
|
||||||
public static function getRelations(): array
|
public static function getRelations(): array
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
namespace App\Filament\Resources\MountResource\Pages;
|
namespace App\Filament\Resources\MountResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\MountResource;
|
use App\Filament\Resources\MountResource;
|
||||||
use App\Services\Servers\ServerCreationService;
|
|
||||||
use Filament\Forms\Components\Group;
|
use Filament\Forms\Components\Group;
|
||||||
use Filament\Forms\Components\Section;
|
use Filament\Forms\Components\Section;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
|
@ -98,6 +98,12 @@ class EditMount extends EditRecord
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make(),
|
Actions\DeleteAction::make(),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,11 @@ class NodeResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Node::class;
|
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 $navigationIcon = 'tabler-server-2';
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'name';
|
protected static ?string $recordTitleAttribute = 'name';
|
||||||
|
@ -4,6 +4,7 @@ namespace App\Filament\Resources\NodeResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Resources\NodeResource;
|
use App\Filament\Resources\NodeResource;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Tabs;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
use Illuminate\Support\HtmlString;
|
use Illuminate\Support\HtmlString;
|
||||||
|
|
||||||
@ -17,179 +18,298 @@ class CreateNode extends CreateRecord
|
|||||||
|
|
||||||
public function form(Forms\Form $form): Forms\Form
|
public function form(Forms\Form $form): Forms\Form
|
||||||
{
|
{
|
||||||
return $form
|
return $form->schema([
|
||||||
->columns([
|
Tabs::make('Tabs')
|
||||||
'default' => 2,
|
->columns([
|
||||||
'sm' => 3,
|
'default' => 2,
|
||||||
'md' => 3,
|
'sm' => 3,
|
||||||
'lg' => 4,
|
'md' => 3,
|
||||||
])
|
'lg' => 4,
|
||||||
->schema([
|
])
|
||||||
Forms\Components\TextInput::make('fqdn')
|
->persistTabInQueryString()
|
||||||
->columnSpan(2)
|
->columnSpanFull()
|
||||||
->required()
|
->tabs([
|
||||||
->autofocus()
|
Tabs\Tab::make('Basic Settings')
|
||||||
->live(debounce: 1500)
|
->icon('tabler-server')
|
||||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
->schema([
|
||||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
Forms\Components\TextInput::make('fqdn')
|
||||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
->columnSpan(2)
|
||||||
->helperText(function ($state) {
|
->required()
|
||||||
if (is_ip($state)) {
|
->autofocus()
|
||||||
if (request()->isSecure()) {
|
->live(debounce: 1500)
|
||||||
return '
|
->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.
|
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
|
You must use a domain name, because you cannot get SSL certificates for IP Addresses
|
||||||
';
|
';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return "
|
return "
|
||||||
This is the domain name that points to your node's IP Address.
|
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!
|
If you've already set up this, you can verify it by checking the next field!
|
||||||
";
|
";
|
||||||
})
|
})
|
||||||
->hintColor('danger')
|
->hintColor('danger')
|
||||||
->hint(function ($state) {
|
->hint(function ($state) {
|
||||||
if (is_ip($state) && request()->isSecure()) {
|
if (is_ip($state) && request()->isSecure()) {
|
||||||
return 'You cannot connect to an IP Address over SSL';
|
return 'You cannot connect to an IP Address over SSL';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
})
|
})
|
||||||
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||||
$set('dns', null);
|
$set('dns', null);
|
||||||
$set('ip', null);
|
$set('ip', null);
|
||||||
|
|
||||||
[$subdomain] = str($state)->explode('.', 2);
|
[$subdomain] = str($state)->explode('.', 2);
|
||||||
if (!is_numeric($subdomain)) {
|
if (!is_numeric($subdomain)) {
|
||||||
$set('name', $subdomain);
|
$set('name', $subdomain);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$state || is_ip($state)) {
|
if (!$state || is_ip($state)) {
|
||||||
$set('dns', null);
|
$set('dns', null);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$validRecords = gethostbynamel($state);
|
$validRecords = gethostbynamel($state);
|
||||||
if ($validRecords) {
|
if ($validRecords) {
|
||||||
$set('dns', true);
|
$set('dns', true);
|
||||||
|
|
||||||
$set('ip', collect($validRecords)->first());
|
$set('ip', collect($validRecords)->first());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$set('dns', false);
|
$set('dns', false);
|
||||||
})
|
})
|
||||||
->maxLength(191),
|
->maxLength(191),
|
||||||
|
|
||||||
Forms\Components\TextInput::make('ip')
|
Forms\Components\TextInput::make('ip')
|
||||||
->disabled()
|
->disabled()
|
||||||
->hidden(),
|
->hidden(),
|
||||||
|
|
||||||
Forms\Components\ToggleButtons::make('dns')
|
Forms\Components\ToggleButtons::make('dns')
|
||||||
->label('DNS Record Check')
|
->label('DNS Record Check')
|
||||||
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||||
->disabled()
|
->disabled()
|
||||||
->inline()
|
->inline()
|
||||||
->default(null)
|
->default(null)
|
||||||
->hint(fn (Forms\Get $get) => $get('ip'))
|
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||||
->hintColor('success')
|
->hintColor('success')
|
||||||
->options([
|
->options([
|
||||||
true => 'Valid',
|
true => 'Valid',
|
||||||
false => 'Invalid',
|
false => 'Invalid',
|
||||||
])
|
])
|
||||||
->colors([
|
->colors([
|
||||||
true => 'success',
|
true => 'success',
|
||||||
false => 'danger',
|
false => 'danger',
|
||||||
])
|
])
|
||||||
->columnSpan([
|
->columnSpan([
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
'sm' => 1,
|
'sm' => 1,
|
||||||
'md' => 1,
|
'md' => 1,
|
||||||
'lg' => 1,
|
'lg' => 1,
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Forms\Components\TextInput::make('daemon_listen')
|
Forms\Components\TextInput::make('daemon_listen')
|
||||||
->columnSpan([
|
->columnSpan([
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
'sm' => 1,
|
'sm' => 1,
|
||||||
'md' => 1,
|
'md' => 1,
|
||||||
'lg' => 1,
|
'lg' => 1,
|
||||||
])
|
])
|
||||||
->label(trans('strings.port'))
|
->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.')
|
->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)
|
->minValue(0)
|
||||||
->maxValue(65536)
|
->maxValue(65536)
|
||||||
->default(8080)
|
->default(8080)
|
||||||
->required()
|
->required()
|
||||||
->integer(),
|
->integer(),
|
||||||
|
|
||||||
Forms\Components\TextInput::make('name')
|
Forms\Components\TextInput::make('name')
|
||||||
->label('Display Name')
|
->label('Display Name')
|
||||||
->columnSpan([
|
->columnSpan([
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
'sm' => 1,
|
'sm' => 1,
|
||||||
'md' => 1,
|
'md' => 1,
|
||||||
'lg' => 2,
|
'lg' => 2,
|
||||||
])
|
])
|
||||||
->required()
|
->required()
|
||||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||||
->helperText('This name is for display only and can be changed later.')
|
->helperText('This name is for display only and can be changed later.')
|
||||||
->maxLength(100),
|
->maxLength(100),
|
||||||
|
|
||||||
Forms\Components\ToggleButtons::make('scheme')
|
Forms\Components\ToggleButtons::make('scheme')
|
||||||
->label('Communicate over SSL')
|
->label('Communicate over SSL')
|
||||||
->columnSpan([
|
->columnSpan([
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
'sm' => 1,
|
'sm' => 1,
|
||||||
'md' => 1,
|
'md' => 1,
|
||||||
'lg' => 1,
|
'lg' => 1,
|
||||||
])
|
])
|
||||||
->required()
|
->required()
|
||||||
->inline()
|
->inline()
|
||||||
->helperText(function (Forms\Get $get) {
|
->helperText(function (Forms\Get $get) {
|
||||||
if (request()->isSecure()) {
|
if (request()->isSecure()) {
|
||||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_ip($get('fqdn'))) {
|
if (is_ip($get('fqdn'))) {
|
||||||
return 'An IP address cannot use SSL.';
|
return 'An IP address cannot use SSL.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
})
|
})
|
||||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||||
->options([
|
->options([
|
||||||
'http' => 'HTTP',
|
'http' => 'HTTP',
|
||||||
'https' => 'HTTPS (SSL)',
|
'https' => 'HTTPS (SSL)',
|
||||||
])
|
])
|
||||||
->colors([
|
->colors([
|
||||||
'http' => 'warning',
|
'http' => 'warning',
|
||||||
'https' => 'success',
|
'https' => 'success',
|
||||||
])
|
])
|
||||||
->icons([
|
->icons([
|
||||||
'http' => 'tabler-lock-open-off',
|
'http' => 'tabler-lock-open-off',
|
||||||
'https' => 'tabler-lock',
|
'https' => 'tabler-lock',
|
||||||
])
|
])
|
||||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||||
|
]),
|
||||||
Forms\Components\Textarea::make('description')
|
Tabs\Tab::make('Advanced Settings')
|
||||||
->label('strings.description')
|
->icon('tabler-server-cog')
|
||||||
->hidden()
|
->schema([
|
||||||
->columnSpan([
|
Forms\Components\TextInput::make('upload_size')
|
||||||
'default' => 1,
|
->label('Upload Limit')
|
||||||
'sm' => 1,
|
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||||
'md' => 2,
|
->columnSpan(1)
|
||||||
'lg' => 4,
|
->numeric()->required()
|
||||||
])
|
->default(256)
|
||||||
->rows(5),
|
->minValue(1)
|
||||||
|
->maxValue(1024)
|
||||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
->suffix('MiB'),
|
||||||
]);
|
Forms\Components\ToggleButtons::make('public')
|
||||||
|
->label('Automatic Allocation')->inline()
|
||||||
|
->default(true)
|
||||||
|
->columnSpan(1)
|
||||||
|
->options([
|
||||||
|
true => 'Yes',
|
||||||
|
false => 'No',
|
||||||
|
])
|
||||||
|
->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('%'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getRedirectUrlParameters(): array
|
protected function getRedirectUrlParameters(): array
|
||||||
|
@ -32,13 +32,293 @@ class EditNode extends EditRecord
|
|||||||
->tabs([
|
->tabs([
|
||||||
Tabs\Tab::make('Basic Settings')
|
Tabs\Tab::make('Basic Settings')
|
||||||
->icon('tabler-server')
|
->icon('tabler-server')
|
||||||
->schema((new CreateNode())->form($form)->getComponents()),
|
->schema([
|
||||||
// Tabs\Tab::make('Advanced Settings')
|
Forms\Components\TextInput::make('fqdn')
|
||||||
// ->icon('tabler-server-cog')
|
->columnSpan(2)
|
||||||
// ->schema([
|
->required()
|
||||||
// Forms\Components\Placeholder::make('Coming soon!'),
|
->autofocus()
|
||||||
// ]),
|
->live(debounce: 1500)
|
||||||
Tabs\Tab::make('Configuration')
|
->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')
|
->icon('tabler-code')
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\Placeholder::make('instructions')
|
Forms\Components\Placeholder::make('instructions')
|
||||||
@ -66,18 +346,17 @@ class EditNode extends EditRecord
|
|||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getSteps(): array
|
protected function getFormActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [];
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make()
|
Actions\DeleteAction::make()
|
||||||
->disabled(fn (Node $node) => $node->servers()->count() > 0)
|
->disabled(fn (Node $node) => $node->servers()->count() > 0)
|
||||||
->label(fn (Node $node) => $node->servers()->count() > 0 ? 'Node Has Servers' : 'Delete'),
|
->label(fn (Node $node) => $node->servers()->count() > 0 ? 'Node Has Servers' : 'Delete'),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,15 +42,15 @@ class ListNodes extends ListRecords
|
|||||||
->visibleFrom('sm')
|
->visibleFrom('sm')
|
||||||
->icon('tabler-device-desktop-analytics')
|
->icon('tabler-device-desktop-analytics')
|
||||||
->numeric()
|
->numeric()
|
||||||
->suffix(' GB')
|
->suffix(' GiB')
|
||||||
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
|
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Tables\Columns\TextColumn::make('disk')
|
Tables\Columns\TextColumn::make('disk')
|
||||||
->visibleFrom('sm')
|
->visibleFrom('sm')
|
||||||
->icon('tabler-file')
|
->icon('tabler-file')
|
||||||
->numeric()
|
->numeric()
|
||||||
->suffix(' GB')
|
->suffix(' GiB')
|
||||||
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
|
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Tables\Columns\IconColumn::make('scheme')
|
Tables\Columns\IconColumn::make('scheme')
|
||||||
->visibleFrom('xl')
|
->visibleFrom('xl')
|
||||||
|
@ -43,10 +43,13 @@ class AllocationsRelationManager extends RelationManager
|
|||||||
Tables\Columns\TextColumn::make('server.name')
|
Tables\Columns\TextColumn::make('server.name')
|
||||||
->label('Server')
|
->label('Server')
|
||||||
->icon('tabler-brand-docker')
|
->icon('tabler-brand-docker')
|
||||||
|
->searchable()
|
||||||
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
|
->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'),
|
->label('Alias'),
|
||||||
Tables\Columns\TextColumn::make('ip')
|
Tables\Columns\TextInputColumn::make('ip')
|
||||||
|
->searchable()
|
||||||
->label('IP'),
|
->label('IP'),
|
||||||
Tables\Columns\TextColumn::make('port')
|
Tables\Columns\TextColumn::make('port')
|
||||||
->searchable()
|
->searchable()
|
||||||
@ -126,6 +129,8 @@ class AllocationsRelationManager extends RelationManager
|
|||||||
$ports = $sortedPorts;
|
$ports = $sortedPorts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||||
|
|
||||||
if ($update) {
|
if ($update) {
|
||||||
$set('allocation_ports', $ports->all());
|
$set('allocation_ports', $ports->all());
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,8 @@ class NodeMemoryChart extends ChartWidget
|
|||||||
/** @var Node $node */
|
/** @var Node $node */
|
||||||
$node = $this->record;
|
$node = $this->record;
|
||||||
|
|
||||||
$total = $node->statistics()['memory_total'] ?? 0;
|
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||||
$used = $node->statistics()['memory_used'] ?? 0;
|
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||||
$unused = $total - $used;
|
$unused = $total - $used;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -40,8 +40,8 @@ class NodeStorageChart extends ChartWidget
|
|||||||
/** @var Node $node */
|
/** @var Node $node */
|
||||||
$node = $this->record;
|
$node = $this->record;
|
||||||
|
|
||||||
$total = $node->statistics()['disk_total'] ?? 0;
|
$total = ($node->statistics()['disk_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||||
$used = $node->statistics()['disk_used'] ?? 0;
|
$used = ($node->statistics()['disk_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||||
$unused = $total - $used;
|
$unused = $total - $used;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -10,6 +10,11 @@ class ServerResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Server::class;
|
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 $navigationIcon = 'tabler-brand-docker';
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'name';
|
protected static ?string $recordTitleAttribute = 'name';
|
||||||
|
@ -487,11 +487,12 @@ class CreateServer extends CreateRecord
|
|||||||
->dehydratedWhenHidden()
|
->dehydratedWhenHidden()
|
||||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||||
->label('Memory Limit')->inlineLabel()
|
->label('Memory Limit')->inlineLabel()
|
||||||
->suffix('MB')
|
->suffix('MiB')
|
||||||
->default(0)
|
->default(0)
|
||||||
->required()
|
->required()
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->numeric(),
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Forms\Components\Grid::make()
|
Forms\Components\Grid::make()
|
||||||
@ -517,11 +518,12 @@ class CreateServer extends CreateRecord
|
|||||||
->dehydratedWhenHidden()
|
->dehydratedWhenHidden()
|
||||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||||
->label('Disk Space Limit')->inlineLabel()
|
->label('Disk Space Limit')->inlineLabel()
|
||||||
->suffix('MB')
|
->suffix('MiB')
|
||||||
->default(0)
|
->default(0)
|
||||||
->required()
|
->required()
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->numeric(),
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Forms\Components\Grid::make()
|
Forms\Components\Grid::make()
|
||||||
@ -551,7 +553,9 @@ class CreateServer extends CreateRecord
|
|||||||
->default(0)
|
->default(0)
|
||||||
->required()
|
->required()
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->numeric(),
|
->numeric()
|
||||||
|
->minValue(0)
|
||||||
|
->helperText('100% equals one logical thread'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Forms\Components\Grid::make()
|
Forms\Components\Grid::make()
|
||||||
@ -593,7 +597,7 @@ class CreateServer extends CreateRecord
|
|||||||
})
|
})
|
||||||
->label('Swap Memory')
|
->label('Swap Memory')
|
||||||
->default(0)
|
->default(0)
|
||||||
->suffix('MB')
|
->suffix('MiB')
|
||||||
->minValue(-1)
|
->minValue(-1)
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->inlineLabel()
|
->inlineLabel()
|
||||||
@ -610,7 +614,7 @@ class CreateServer extends CreateRecord
|
|||||||
->columns(4)
|
->columns(4)
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\ToggleButtons::make('oom_disabled')
|
Forms\Components\ToggleButtons::make('oom_killer')
|
||||||
->label('OOM Killer')
|
->label('OOM Killer')
|
||||||
->inlineLabel()->inline()
|
->inlineLabel()->inline()
|
||||||
->default(false)
|
->default(false)
|
||||||
|
@ -244,35 +244,57 @@ class EditServer extends EditRecord
|
|||||||
]))
|
]))
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\Repeater::make('server_variables')
|
Forms\Components\Repeater::make('server_variables')
|
||||||
->label('')
|
|
||||||
->relationship('serverVariables')
|
->relationship('serverVariables')
|
||||||
->grid()
|
->grid()
|
||||||
->deletable(false)
|
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||||
->addable(false)
|
foreach ($data as $key => $value) {
|
||||||
->schema([
|
if (!isset($data['variable_value'])) {
|
||||||
Forms\Components\TextInput::make('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([
|
->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], [
|
$validator = Validator::make(['validatorkey' => $value], [
|
||||||
'validatorkey' => $variable->variable->rules,
|
'validatorkey' => $serverVariable->variable->rules,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
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);
|
$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),
|
->columnSpan(2),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
@ -311,10 +333,11 @@ class EditServer extends EditRecord
|
|||||||
->dehydratedWhenHidden()
|
->dehydratedWhenHidden()
|
||||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||||
->label('Memory Limit')->inlineLabel()
|
->label('Memory Limit')->inlineLabel()
|
||||||
->suffix('MB')
|
->suffix('MiB')
|
||||||
->required()
|
->required()
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->numeric(),
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Forms\Components\Grid::make()
|
Forms\Components\Grid::make()
|
||||||
@ -340,10 +363,11 @@ class EditServer extends EditRecord
|
|||||||
->dehydratedWhenHidden()
|
->dehydratedWhenHidden()
|
||||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||||
->label('Disk Space Limit')->inlineLabel()
|
->label('Disk Space Limit')->inlineLabel()
|
||||||
->suffix('MB')
|
->suffix('MiB')
|
||||||
->required()
|
->required()
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->numeric(),
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Forms\Components\Grid::make()
|
Forms\Components\Grid::make()
|
||||||
@ -372,7 +396,8 @@ class EditServer extends EditRecord
|
|||||||
->suffix('%')
|
->suffix('%')
|
||||||
->required()
|
->required()
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->numeric(),
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Forms\Components\Grid::make()
|
Forms\Components\Grid::make()
|
||||||
@ -417,7 +442,7 @@ class EditServer extends EditRecord
|
|||||||
'limited', false => false,
|
'limited', false => false,
|
||||||
})
|
})
|
||||||
->label('Swap Memory')->inlineLabel()
|
->label('Swap Memory')->inlineLabel()
|
||||||
->suffix('MB')
|
->suffix('MiB')
|
||||||
->minValue(-1)
|
->minValue(-1)
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->required()
|
->required()
|
||||||
@ -432,7 +457,7 @@ class EditServer extends EditRecord
|
|||||||
->columns(4)
|
->columns(4)
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\ToggleButtons::make('oom_disabled')
|
Forms\Components\ToggleButtons::make('oom_killer')
|
||||||
->label('OOM Killer')->inlineLabel()->inline()
|
->label('OOM Killer')->inlineLabel()->inline()
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->options([
|
->options([
|
||||||
@ -487,13 +512,6 @@ class EditServer extends EditRecord
|
|||||||
->color('danger')
|
->color('danger')
|
||||||
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
||||||
->requiresConfirmation(),
|
->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')
|
Actions\Action::make('console')
|
||||||
->label('Console')
|
->label('Console')
|
||||||
->icon('tabler-terminal')
|
->icon('tabler-terminal')
|
||||||
@ -520,4 +538,35 @@ class EditServer extends EditRecord
|
|||||||
ServerResource\RelationManagers\AllocationsRelationManager::class,
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ class AllocationsRelationManager extends RelationManager
|
|||||||
// ->actions
|
// ->actions
|
||||||
// ->groups
|
// ->groups
|
||||||
->columns([
|
->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('ip')->label('IP'),
|
||||||
Tables\Columns\TextColumn::make('port')->label('Port'),
|
Tables\Columns\TextColumn::make('port')->label('Port'),
|
||||||
Tables\Columns\IconColumn::make('primary')
|
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'),
|
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
|
||||||
])
|
])
|
||||||
->headerActions([
|
->headerActions([
|
||||||
Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||||
//Tables\Actions\AssociateAction::make()->label('Add Allocation'),
|
//TODO Tables\Actions\AssociateAction::make()->label('Add Allocation'),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
@ -11,6 +11,10 @@ class UserResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = User::class;
|
protected static ?string $model = User::class;
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count();
|
||||||
|
}
|
||||||
protected static ?string $navigationIcon = 'tabler-users';
|
protected static ?string $navigationIcon = 'tabler-users';
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'username';
|
protected static ?string $recordTitleAttribute = 'username';
|
||||||
|
@ -67,6 +67,12 @@ class EditUser extends EditRecord
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make(),
|
Actions\DeleteAction::make(),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Servers;
|
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\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
|
class ServerTransferController extends Controller
|
||||||
{
|
{
|
||||||
@ -25,30 +16,10 @@ class ServerTransferController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private AlertsMessageBag $alert,
|
private AlertsMessageBag $alert,
|
||||||
private ConnectionInterface $connection,
|
private TransferServerService $transferServerService,
|
||||||
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.
|
* Starts a transfer of a server to a new node.
|
||||||
*
|
*
|
||||||
@ -62,85 +33,12 @@ class ServerTransferController extends Controller
|
|||||||
'allocation_additional' => 'nullable',
|
'allocation_additional' => 'nullable',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$node_id = $validatedData['node_id'];
|
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||||
$allocation_id = intval($validatedData['allocation_id']);
|
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
|
||||||
$additional_allocations = array_map('intval', $validatedData['allocation_additional'] ?? []);
|
} else {
|
||||||
|
|
||||||
// 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();
|
$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;
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
|
|
||||||
|
|
||||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ class ServersController extends Controller
|
|||||||
$this->buildModificationService->handle($server, $request->only([
|
$this->buildModificationService->handle($server, $request->only([
|
||||||
'allocation_id', 'add_allocations', 'remove_allocations',
|
'allocation_id', 'add_allocations', 'remove_allocations',
|
||||||
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
|
'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) {
|
} catch (DataValidationException $exception) {
|
||||||
throw new ValidationException($exception->getValidator());
|
throw new ValidationException($exception->getValidator());
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\Application\Servers;
|
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\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
|
class ServerManagementController extends ApplicationApiController
|
||||||
{
|
{
|
||||||
@ -16,7 +18,9 @@ class ServerManagementController extends ApplicationApiController
|
|||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ReinstallServerService $reinstallServerService,
|
private ReinstallServerService $reinstallServerService,
|
||||||
private SuspensionService $suspensionService
|
private SuspensionService $suspensionService,
|
||||||
|
private TransferServerService $transferServerService,
|
||||||
|
private DaemonServerRepository $daemonServerRepository,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@ -57,4 +61,44 @@ class ServerManagementController extends ApplicationApiController
|
|||||||
|
|
||||||
return $this->returnNoContent();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -32,7 +32,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
|||||||
'startup' => $rules['startup'],
|
'startup' => $rules['startup'],
|
||||||
'environment' => 'present|array',
|
'environment' => 'present|array',
|
||||||
'skip_scripts' => 'sometimes|boolean',
|
'skip_scripts' => 'sometimes|boolean',
|
||||||
'oom_disabled' => 'sometimes|boolean',
|
'oom_killer' => 'sometimes|boolean',
|
||||||
|
|
||||||
// Resource limitations
|
// Resource limitations
|
||||||
'limits' => 'required|array',
|
'limits' => 'required|array',
|
||||||
@ -94,7 +94,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
|||||||
'database_limit' => array_get($data, 'feature_limits.databases'),
|
'database_limit' => array_get($data, 'feature_limits.databases'),
|
||||||
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
|
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
|
||||||
'backup_limit' => array_get($data, 'feature_limits.backups'),
|
'backup_limit' => array_get($data, 'feature_limits.backups'),
|
||||||
'oom_disabled' => array_get($data, 'oom_disabled'),
|
'oom_killer' => array_get($data, 'oom_killer'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'allocation' => $rules['allocation_id'],
|
'allocation' => $rules['allocation_id'],
|
||||||
'oom_disabled' => $rules['oom_disabled'],
|
'oom_killer' => $rules['oom_killer'],
|
||||||
|
|
||||||
'limits' => 'sometimes|array',
|
'limits' => 'sometimes|array',
|
||||||
'limits.memory' => $this->requiredToOptional('memory', $rules['memory'], true),
|
'limits.memory' => $this->requiredToOptional('memory', $rules['memory'], true),
|
||||||
|
@ -19,9 +19,11 @@ class StoreScheduleRequest extends ViewScheduleRequest
|
|||||||
return [
|
return [
|
||||||
'name' => $rules['name'],
|
'name' => $rules['name'],
|
||||||
'is_active' => array_merge(['filled'], $rules['is_active']),
|
'is_active' => array_merge(['filled'], $rules['is_active']),
|
||||||
|
'only_when_online' => $rules['only_when_online'],
|
||||||
'minute' => $rules['cron_minute'],
|
'minute' => $rules['cron_minute'],
|
||||||
'hour' => $rules['cron_hour'],
|
'hour' => $rules['cron_hour'],
|
||||||
'day_of_month' => $rules['cron_day_of_month'],
|
'day_of_month' => $rules['cron_day_of_month'],
|
||||||
|
'month' => $rules['cron_month'],
|
||||||
'day_of_week' => $rules['cron_day_of_week'],
|
'day_of_week' => $rules['cron_day_of_week'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
|||||||
* @property int $io
|
* @property int $io
|
||||||
* @property int $cpu
|
* @property int $cpu
|
||||||
* @property string|null $threads
|
* @property string|null $threads
|
||||||
* @property bool $oom_disabled
|
* @property bool $oom_killer
|
||||||
* @property int $allocation_id
|
* @property int $allocation_id
|
||||||
* @property int $egg_id
|
* @property int $egg_id
|
||||||
* @property string $startup
|
* @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 whereMemory($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereName($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 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 whereOwnerId($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereSkipScripts($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Server whereSkipScripts($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereStartup($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Server whereStartup($value)
|
||||||
@ -124,7 +124,7 @@ class Server extends Model
|
|||||||
*/
|
*/
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
'status' => ServerState::Installing,
|
'status' => ServerState::Installing,
|
||||||
'oom_disabled' => true,
|
'oom_killer' => false,
|
||||||
'installed_at' => null,
|
'installed_at' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ class Server extends Model
|
|||||||
'io' => 'required|numeric|between:0,1000',
|
'io' => 'required|numeric|between:0,1000',
|
||||||
'cpu' => 'required|numeric|min:0',
|
'cpu' => 'required|numeric|min:0',
|
||||||
'threads' => 'nullable|regex:/^[0-9-,]+$/',
|
'threads' => 'nullable|regex:/^[0-9-,]+$/',
|
||||||
'oom_disabled' => 'sometimes|boolean',
|
'oom_killer' => 'sometimes|boolean',
|
||||||
'disk' => 'required|numeric|min:0',
|
'disk' => 'required|numeric|min:0',
|
||||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||||
'egg_id' => 'required|exists:eggs,id',
|
'egg_id' => 'required|exists:eggs,id',
|
||||||
@ -174,7 +174,7 @@ class Server extends Model
|
|||||||
'disk' => 'integer',
|
'disk' => 'integer',
|
||||||
'io' => 'integer',
|
'io' => 'integer',
|
||||||
'cpu' => 'integer',
|
'cpu' => 'integer',
|
||||||
'oom_disabled' => 'boolean',
|
'oom_killer' => 'boolean',
|
||||||
'allocation_id' => 'integer',
|
'allocation_id' => 'integer',
|
||||||
'egg_id' => 'integer',
|
'egg_id' => 'integer',
|
||||||
'database_limit' => 'integer',
|
'database_limit' => 'integer',
|
||||||
|
@ -19,6 +19,7 @@ use Illuminate\Routing\Middleware\SubstituteBindings;
|
|||||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
|
use Saade\FilamentLaravelLog\FilamentLaravelLogPlugin;
|
||||||
|
|
||||||
class AdminPanelProvider extends PanelProvider
|
class AdminPanelProvider extends PanelProvider
|
||||||
{
|
{
|
||||||
@ -35,8 +36,8 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
->default()
|
->default()
|
||||||
->id('admin')
|
->id('admin')
|
||||||
->path('admin')
|
->path('admin')
|
||||||
|
->topNavigation(true)
|
||||||
->login()
|
->login()
|
||||||
->brandName('Pelican')
|
|
||||||
->homeUrl('/')
|
->homeUrl('/')
|
||||||
->favicon('/pelican.ico')
|
->favicon('/pelican.ico')
|
||||||
->profile(EditProfile::class, false)
|
->profile(EditProfile::class, false)
|
||||||
@ -74,6 +75,13 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
])
|
])
|
||||||
->authMiddleware([
|
->authMiddleware([
|
||||||
Authenticate::class,
|
Authenticate::class,
|
||||||
]);
|
])
|
||||||
|
->plugin(
|
||||||
|
FilamentLaravelLogPlugin::make()
|
||||||
|
->navigationLabel('Logs')
|
||||||
|
->navigationIcon('tabler-file-info')
|
||||||
|
->slug('logs')
|
||||||
|
->authorize(fn () => auth()->user()->root_admin)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
* 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
|
* make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted
|
||||||
|
@ -40,8 +40,12 @@ class BuildModificationService
|
|||||||
throw_unless($existingAllocation, new DisplayException('The requested default allocation is not currently assigned to this server.'));
|
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.
|
// 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, [
|
$server->forceFill(array_merge($merge, [
|
||||||
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
|
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
|
||||||
|
@ -59,14 +59,12 @@ class ServerConfigurationStructureService
|
|||||||
'cpu_limit' => $server->cpu,
|
'cpu_limit' => $server->cpu,
|
||||||
'threads' => $server->threads,
|
'threads' => $server->threads,
|
||||||
'disk_space' => $server->disk,
|
'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' => [
|
'container' => [
|
||||||
'image' => $server->image,
|
'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,
|
'requires_rebuild' => false,
|
||||||
],
|
],
|
||||||
'allocations' => [
|
'allocations' => [
|
||||||
@ -110,7 +108,7 @@ class ServerConfigurationStructureService
|
|||||||
return $item->pluck('port');
|
return $item->pluck('port');
|
||||||
})->toArray(),
|
})->toArray(),
|
||||||
'env' => $this->environment->handle($server),
|
'env' => $this->environment->handle($server),
|
||||||
'oom_disabled' => $server->oom_disabled,
|
'oom_disabled' => !$server->oom_killer,
|
||||||
'memory' => (int) $server->memory,
|
'memory' => (int) $server->memory,
|
||||||
'swap' => (int) $server->swap,
|
'swap' => (int) $server->swap,
|
||||||
'io' => (int) $server->io,
|
'io' => (int) $server->io,
|
||||||
|
@ -47,6 +47,10 @@ class ServerCreationService
|
|||||||
*/
|
*/
|
||||||
public function handle(array $data, DeploymentObject $deployment = null): Server
|
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
|
// 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.
|
// that the server should use, and assign the node from that allocation.
|
||||||
if ($deployment instanceof DeploymentObject) {
|
if ($deployment instanceof DeploymentObject) {
|
||||||
@ -142,7 +146,7 @@ class ServerCreationService
|
|||||||
'io' => Arr::get($data, 'io'),
|
'io' => Arr::get($data, 'io'),
|
||||||
'cpu' => Arr::get($data, 'cpu'),
|
'cpu' => Arr::get($data, 'cpu'),
|
||||||
'threads' => Arr::get($data, 'threads'),
|
'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'),
|
'allocation_id' => Arr::get($data, 'allocation_id'),
|
||||||
'egg_id' => Arr::get($data, 'egg_id'),
|
'egg_id' => Arr::get($data, 'egg_id'),
|
||||||
'startup' => Arr::get($data, 'startup'),
|
'startup' => Arr::get($data, 'startup'),
|
||||||
|
131
app/Services/Servers/TransferServerService.php
Normal file
131
app/Services/Servers/TransferServerService.php
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Transformers\Api\Application;
|
namespace App\Transformers\Api\Application;
|
||||||
|
|
||||||
|
use App\Models\Node;
|
||||||
use App\Models\Database;
|
use App\Models\Database;
|
||||||
use App\Models\DatabaseHost;
|
use App\Models\DatabaseHost;
|
||||||
|
use League\Fractal\Resource\Item;
|
||||||
use League\Fractal\Resource\Collection;
|
use League\Fractal\Resource\Collection;
|
||||||
use League\Fractal\Resource\NullResource;
|
use League\Fractal\Resource\NullResource;
|
||||||
use App\Services\Acl\Api\AdminAcl;
|
use App\Services\Acl\Api\AdminAcl;
|
||||||
@ -12,6 +14,7 @@ class DatabaseHostTransformer extends BaseTransformer
|
|||||||
{
|
{
|
||||||
protected array $availableIncludes = [
|
protected array $availableIncludes = [
|
||||||
'databases',
|
'databases',
|
||||||
|
'node',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,4 +57,20 @@ class DatabaseHostTransformer extends BaseTransformer
|
|||||||
|
|
||||||
return $this->collection($model->getRelation('databases'), $this->makeTransformer(ServerDatabaseTransformer::class), Database::RESOURCE_NAME);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,9 @@ class ServerTransformer extends BaseTransformer
|
|||||||
'io' => $server->io,
|
'io' => $server->io,
|
||||||
'cpu' => $server->cpu,
|
'cpu' => $server->cpu,
|
||||||
'threads' => $server->threads,
|
'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' => [
|
'feature_limits' => [
|
||||||
'databases' => $server->database_limit,
|
'databases' => $server->database_limit,
|
||||||
|
@ -56,7 +56,9 @@ class ServerTransformer extends BaseClientTransformer
|
|||||||
'io' => $server->io,
|
'io' => $server->io,
|
||||||
'cpu' => $server->cpu,
|
'cpu' => $server->cpu,
|
||||||
'threads' => $server->threads,
|
'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)),
|
'invocation' => $service->handle($server, !$user->can(Permission::ACTION_STARTUP_READ, $server)),
|
||||||
'docker_image' => $server->image,
|
'docker_image' => $server->image,
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"prologue/alerts": "^1.2",
|
"prologue/alerts": "^1.2",
|
||||||
"ryangjchandler/blade-tabler-icons": "^2.3",
|
"ryangjchandler/blade-tabler-icons": "^2.3",
|
||||||
"s1lentium/iptools": "~1.2.0",
|
"s1lentium/iptools": "~1.2.0",
|
||||||
|
"saade/filament-laravel-log": "^3.2",
|
||||||
"spatie/laravel-fractal": "^6.1",
|
"spatie/laravel-fractal": "^6.1",
|
||||||
"spatie/laravel-query-builder": "^5.8",
|
"spatie/laravel-query-builder": "^5.8",
|
||||||
"symfony/mailgun-mailer": "^7.0",
|
"symfony/mailgun-mailer": "^7.0",
|
||||||
|
761
composer.lock
generated
761
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ use Illuminate\Support\Facades\Facade;
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'name' => 'Pelican',
|
'name' => env('APP_NAME', 'Pelican'),
|
||||||
|
|
||||||
'version' => 'canary',
|
'version' => 'canary',
|
||||||
|
|
||||||
|
@ -46,6 +46,8 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
|
'client' => env('REDIS_CLIENT', 'predis'),
|
||||||
|
|
||||||
'default' => [
|
'default' => [
|
||||||
'scheme' => env('REDIS_SCHEME', 'tcp'),
|
'scheme' => env('REDIS_SCHEME', 'tcp'),
|
||||||
'path' => env('REDIS_PATH', '/run/redis/redis.sock'),
|
'path' => env('REDIS_PATH', '/run/redis/redis.sock'),
|
||||||
|
@ -35,7 +35,7 @@ class ServerFactory extends Factory
|
|||||||
'io' => 500,
|
'io' => 500,
|
||||||
'cpu' => 0,
|
'cpu' => 0,
|
||||||
'threads' => null,
|
'threads' => null,
|
||||||
'oom_disabled' => 0,
|
'oom_killer' => false,
|
||||||
'startup' => '/bin/bash echo "hello world"',
|
'startup' => '/bin/bash echo "hello world"',
|
||||||
'image' => 'foo/bar:latest',
|
'image' => 'foo/bar:latest',
|
||||||
'allocation_limit' => null,
|
'allocation_limit' => null,
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -495,7 +495,7 @@ CREATE TABLE `servers` (
|
|||||||
`io` int unsigned NOT NULL,
|
`io` int unsigned NOT NULL,
|
||||||
`cpu` int unsigned NOT NULL,
|
`cpu` int unsigned NOT NULL,
|
||||||
`threads` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT 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,
|
`allocation_id` int unsigned NOT NULL,
|
||||||
`egg_id` int unsigned NOT NULL,
|
`egg_id` int unsigned NOT NULL,
|
||||||
`startup` text COLLATE utf8mb4_unicode_ci 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 (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 (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 (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);
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
<env name="SESSION_DRIVER" value="array"/>
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
<env name="MAIL_DRIVER" value="array"/>
|
|
||||||
</php>
|
</php>
|
||||||
<source>
|
<source>
|
||||||
<include>
|
<include>
|
||||||
|
@ -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
@ -1,4 +1,4 @@
|
|||||||

|
<img width="20%" src="https://raw.githubusercontent.com/pelican-dev/panel/main/public/pelican.svg" alt="logo">
|
||||||
|
|
||||||
# Pelican Panel
|
# Pelican Panel
|
||||||
|
|
||||||
|
@ -203,8 +203,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group col-xs-12">
|
<div class="form-group col-xs-12">
|
||||||
<div class="checkbox checkbox-primary no-margin-bottom">
|
<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) }} />
|
<input type="checkbox" id="pOomKiller" name="oom_killer" value="0" {{ \App\Helpers\Utilities::checked('oom_killer', 0) }} />
|
||||||
<label for="pOomDisabled" class="strong">Enable OOM Killer</label>
|
<label for="pOomKiller" class="strong">Enable OOM Killer</label>
|
||||||
</div>
|
</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>
|
<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>
|
||||||
|
@ -74,11 +74,11 @@
|
|||||||
<label for="cpu" class="control-label">OOM Killer</label>
|
<label for="cpu" class="control-label">OOM Killer</label>
|
||||||
<div>
|
<div>
|
||||||
<div class="radio radio-danger radio-inline">
|
<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>
|
<label for="pOomKillerEnabled">Enabled</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="radio radio-success radio-inline">
|
<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>
|
<label for="pOomKillerDisabled">Disabled</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted small">
|
<p class="text-muted small">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="{{ config('scramble.theme', 'dark') }}">
|
<html lang="en" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
@ -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}/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}/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}/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}', [Application\Servers\ServerController::class, 'delete']);
|
||||||
Route::delete('/{server:id}/{force?}', [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('/', [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');
|
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']);
|
||||||
|
});
|
||||||
|
@ -57,7 +57,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase
|
|||||||
$response = $this->actingAs($user)->postJson("/api/client/servers/$server->uuid/schedules", []);
|
$response = $this->actingAs($user)->postJson("/api/client/servers/$server->uuid/schedules", []);
|
||||||
|
|
||||||
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
|
$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.code", 'ValidationException');
|
||||||
$response->assertJsonPath("errors.$i.meta.rule", 'required');
|
$response->assertJsonPath("errors.$i.meta.rule", 'required');
|
||||||
$response->assertJsonPath("errors.$i.meta.source_field", $field);
|
$response->assertJsonPath("errors.$i.meta.source_field", $field);
|
||||||
@ -67,6 +67,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase
|
|||||||
->postJson("/api/client/servers/$server->uuid/schedules", [
|
->postJson("/api/client/servers/$server->uuid/schedules", [
|
||||||
'name' => 'Testing',
|
'name' => 'Testing',
|
||||||
'is_active' => 'no',
|
'is_active' => 'no',
|
||||||
|
'only_when_online' => 'false',
|
||||||
'minute' => '*',
|
'minute' => '*',
|
||||||
'hour' => '*',
|
'hour' => '*',
|
||||||
'day_of_month' => '*',
|
'day_of_month' => '*',
|
||||||
|
@ -114,7 +114,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
|||||||
$this->daemonServerRepository->expects('sync')->withNoArgs()->andReturnUndefined();
|
$this->daemonServerRepository->expects('sync')->withNoArgs()->andReturnUndefined();
|
||||||
|
|
||||||
$response = $this->getService()->handle($server, [
|
$response = $this->getService()->handle($server, [
|
||||||
'oom_disabled' => false,
|
'oom_killer' => false,
|
||||||
'memory' => 256,
|
'memory' => 256,
|
||||||
'swap' => 128,
|
'swap' => 128,
|
||||||
'io' => 600,
|
'io' => 600,
|
||||||
@ -126,7 +126,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
|||||||
'allocation_limit' => 20,
|
'allocation_limit' => 20,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertFalse($response->oom_disabled);
|
$this->assertFalse($response->oom_killer);
|
||||||
$this->assertSame(256, $response->memory);
|
$this->assertSame(256, $response->memory);
|
||||||
$this->assertSame(128, $response->swap);
|
$this->assertSame(128, $response->swap);
|
||||||
$this->assertSame(600, $response->io);
|
$this->assertSame(600, $response->io);
|
||||||
|
@ -138,7 +138,7 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
|||||||
$this->assertSame($allocations[4]->id, $response->allocations[1]->id);
|
$this->assertSame($allocations[4]->id, $response->allocations[1]->id);
|
||||||
|
|
||||||
$this->assertFalse($response->isSuspended());
|
$this->assertFalse($response->isSuspended());
|
||||||
$this->assertTrue($response->oom_disabled);
|
$this->assertFalse($response->oom_killer);
|
||||||
$this->assertSame(0, $response->database_limit);
|
$this->assertSame(0, $response->database_limit);
|
||||||
$this->assertSame(0, $response->allocation_limit);
|
$this->assertSame(0, $response->allocation_limit);
|
||||||
$this->assertSame(0, $response->backup_limit);
|
$this->assertSame(0, $response->backup_limit);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user