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
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=file
|
||||
|
||||
HASHIDS_SALT=
|
||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1,2 +1,2 @@
|
||||
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;
|
||||
public const CACHE_DRIVERS = [
|
||||
'redis' => 'Redis',
|
||||
'memcached' => 'Memcached',
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'redis' => 'Redis',
|
||||
'memcached' => 'Memcached',
|
||||
'database' => 'MySQL Database',
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'MySQL Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'MySQL Database (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'MySQL Database',
|
||||
'sync' => 'Sync (recommended)',
|
||||
];
|
||||
|
||||
protected $description = 'Configure basic environment settings for the Panel.';
|
||||
@ -86,7 +83,7 @@ class AppSettingsCommand extends Command
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('queue.default', 'sync');
|
||||
$selected = config('queue.default', 'database');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
|
@ -31,7 +31,7 @@ class EmailSettingsCommand extends Command
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$this->variables['MAIL_DRIVER'] = $this->option('driver') ?? $this->choice(
|
||||
$this->variables['MAIL_MAILER'] = $this->option('driver') ?? $this->choice(
|
||||
trans('command/messages.environment.mail.ask_driver'),
|
||||
[
|
||||
'log' => 'Log',
|
||||
@ -41,10 +41,10 @@ class EmailSettingsCommand extends Command
|
||||
'mandrill' => 'Mandrill',
|
||||
'postmark' => 'Postmark',
|
||||
],
|
||||
'smtp',
|
||||
env('MAIL_MAILER', env('MAIL_DRIVER', 'smtp')),
|
||||
);
|
||||
|
||||
$method = 'setup' . studly_case($this->variables['MAIL_DRIVER']) . 'DriverVariables';
|
||||
$method = 'setup' . studly_case($this->variables['MAIL_MAILER']) . 'DriverVariables';
|
||||
if (method_exists($this, $method)) {
|
||||
$this->{$method}();
|
||||
}
|
||||
|
@ -7,9 +7,7 @@ use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
|
@ -13,6 +13,10 @@ class ApiKeyResource extends Resource
|
||||
protected static ?string $model = ApiKey::class;
|
||||
protected static ?string $label = 'API Key';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
}
|
||||
protected static ?string $navigationIcon = 'tabler-key';
|
||||
|
||||
public static function canEdit($record): bool
|
||||
|
@ -10,6 +10,11 @@ class DatabaseHostResource extends Resource
|
||||
{
|
||||
protected static ?string $model = DatabaseHost::class;
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
}
|
||||
|
||||
protected static ?string $label = 'Databases';
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
@ -22,50 +22,59 @@ class CreateDatabaseHost extends CreateRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live()
|
||||
->debounce(500)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Section::make()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->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'])) {
|
||||
$data['password'] = encrypt($data['password']);
|
||||
@ -73,4 +82,17 @@ class CreateDatabaseHost extends CreateRecord
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getCreateFormAction()->formId('form'),
|
||||
];
|
||||
|
||||
}
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,45 +17,54 @@ class EditDatabaseHost extends EditRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live()
|
||||
->debounce(500)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Section::make()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->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'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -63,6 +72,7 @@ class EditDatabaseHost extends EditRecord
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -74,4 +84,16 @@ class EditDatabaseHost extends EditRecord
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
DatabaseHostResource\RelationManagers\DatabasesRelationManager::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ class ListDatabaseHosts extends ListRecords
|
||||
{
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
protected ?string $heading = 'Database Hosts';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
@ -48,7 +50,7 @@ class ListDatabaseHosts extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
Actions\CreateAction::make('create')->label('New Database Host'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
}
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
@ -10,6 +10,10 @@ class EggResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Egg::class;
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
}
|
||||
protected static ?string $navigationIcon = 'tabler-eggs';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
@ -25,12 +25,16 @@ class EditEgg extends EditRecord
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1])
|
||||
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||
Forms\Components\TextInput::make('uuid')
|
||||
->label('Egg UUID')
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'),
|
||||
Forms\Components\TextInput::make('id')
|
||||
->label('Egg ID')
|
||||
->disabled(),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->rows(3)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
@ -203,6 +207,19 @@ class EditEgg extends EditRecord
|
||||
->color('primary')
|
||||
// TODO uses old admin panel export service
|
||||
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
EggResource\RelationManagers\ServersRelationManager::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
}
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-layers-linked';
|
||||
|
||||
public static function getRelations(): array
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Filament\Resources\MountResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MountResource;
|
||||
use App\Services\Servers\ServerCreationService;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
|
@ -98,6 +98,12 @@ class EditMount extends EditRecord
|
||||
{
|
||||
return [
|
||||
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;
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
}
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-server-2';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
@ -4,6 +4,7 @@ namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
@ -17,179 +18,298 @@ class CreateNode extends CreateRecord
|
||||
|
||||
public function form(Forms\Form $form): Forms\Form
|
||||
{
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return '
|
||||
return $form->schema([
|
||||
Tabs::make('Tabs')
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return '
|
||||
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
|
||||
You must use a domain name, because you cannot get SSL certificates for IP Addresses
|
||||
';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
})
|
||||
->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);
|
||||
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);
|
||||
}
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$set('dns', true);
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', collect($validRecords)->first());
|
||||
$set('ip', collect($validRecords)->first());
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(191),
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(191),
|
||||
|
||||
Forms\Components\TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
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\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('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\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.');
|
||||
}
|
||||
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.';
|
||||
}
|
||||
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'),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('strings.description')
|
||||
->hidden()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->rows(5),
|
||||
|
||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
||||
]);
|
||||
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('upload_size')
|
||||
->label('Upload Limit')
|
||||
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
->columnSpan(1)
|
||||
->numeric()->required()
|
||||
->default(256)
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix('MiB'),
|
||||
Forms\Components\ToggleButtons::make('public')
|
||||
->label('Automatic Allocation')->inline()
|
||||
->default(true)
|
||||
->columnSpan(1)
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
])
|
||||
->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
|
||||
|
@ -32,13 +32,293 @@ class EditNode extends EditRecord
|
||||
->tabs([
|
||||
Tabs\Tab::make('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->schema((new CreateNode())->form($form)->getComponents()),
|
||||
// Tabs\Tab::make('Advanced Settings')
|
||||
// ->icon('tabler-server-cog')
|
||||
// ->schema([
|
||||
// Forms\Components\Placeholder::make('Coming soon!'),
|
||||
// ]),
|
||||
Tabs\Tab::make('Configuration')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return '
|
||||
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
|
||||
You must use a domain name, because you cannot get SSL certificates for IP Addresses
|
||||
';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return "
|
||||
This is the domain name that points to your node's IP Address.
|
||||
If you've already set up this, you can verify it by checking the next field!
|
||||
";
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return 'You cannot connect to an IP Address over SSL';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', collect($validRecords)->first());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(191),
|
||||
|
||||
Forms\Components\TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('dns')
|
||||
->label('DNS Record Check')
|
||||
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => 'Valid',
|
||||
false => 'Invalid',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(0)
|
||||
->maxValue(65536)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
|
||||
Forms\Components\ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->inline()
|
||||
->helperText(function (Forms\Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||
}
|
||||
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return 'An IP address cannot use SSL.';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
||||
Tabs\Tab::make('Advanced Settings')
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('id')
|
||||
->label('Node ID')
|
||||
->disabled(),
|
||||
Forms\Components\TextInput::make('uuid')
|
||||
->label('Node UUID')
|
||||
->hintAction(CopyAction::make())
|
||||
->columnSpan(2)
|
||||
->disabled(),
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
->label('Tags')
|
||||
->disabled()
|
||||
->placeholder('Not Implemented')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Not Implemented')
|
||||
->columnSpan(1),
|
||||
Forms\Components\ToggleButtons::make('public')
|
||||
->label('Automatic Allocation')->inline()
|
||||
->columnSpan(1)
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
Forms\Components\ToggleButtons::make('maintenance_mode')
|
||||
->label('Maintenance Mode')->inline()
|
||||
->columnSpan(1)
|
||||
->hinticon('tabler-question-mark')
|
||||
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
|
||||
->options([
|
||||
true => 'Enable',
|
||||
false => 'Disable',
|
||||
])
|
||||
->colors([
|
||||
true => 'danger',
|
||||
false => 'success',
|
||||
]),
|
||||
Forms\Components\TextInput::make('upload_size')
|
||||
->label('Upload Limit')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
->columnStart(4)->columnSpan(1)
|
||||
->numeric()->required()
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix('MiB'),
|
||||
Forms\Components\Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
Forms\Components\TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
Forms\Components\TextInput::make('memory_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->required()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
Forms\Components\Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
Forms\Components\TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
Forms\Components\TextInput::make('disk_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Configuration File')
|
||||
->icon('tabler-code')
|
||||
->schema([
|
||||
Forms\Components\Placeholder::make('instructions')
|
||||
@ -66,18 +346,17 @@ class EditNode extends EditRecord
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getSteps(): array
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
->disabled(fn (Node $node) => $node->servers()->count() > 0)
|
||||
->label(fn (Node $node) => $node->servers()->count() > 0 ? 'Node Has Servers' : 'Delete'),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -42,15 +42,15 @@ class ListNodes extends ListRecords
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-device-desktop-analytics')
|
||||
->numeric()
|
||||
->suffix(' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
|
||||
->suffix(' GiB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('disk')
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-file')
|
||||
->numeric()
|
||||
->suffix(' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
|
||||
->suffix(' GiB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('scheme')
|
||||
->visibleFrom('xl')
|
||||
|
@ -43,10 +43,13 @@ class AllocationsRelationManager extends RelationManager
|
||||
Tables\Columns\TextColumn::make('server.name')
|
||||
->label('Server')
|
||||
->icon('tabler-brand-docker')
|
||||
->searchable()
|
||||
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
|
||||
Tables\Columns\TextColumn::make('ip_alias')
|
||||
Tables\Columns\TextInputColumn::make('ip_alias')
|
||||
->searchable()
|
||||
->label('Alias'),
|
||||
Tables\Columns\TextColumn::make('ip')
|
||||
Tables\Columns\TextInputColumn::make('ip')
|
||||
->searchable()
|
||||
->label('IP'),
|
||||
Tables\Columns\TextColumn::make('port')
|
||||
->searchable()
|
||||
@ -126,6 +129,8 @@ class AllocationsRelationManager extends RelationManager
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ class NodeMemoryChart extends ChartWidget
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
|
||||
$total = $node->statistics()['memory_total'] ?? 0;
|
||||
$used = $node->statistics()['memory_used'] ?? 0;
|
||||
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$unused = $total - $used;
|
||||
|
||||
return [
|
||||
|
@ -40,8 +40,8 @@ class NodeStorageChart extends ChartWidget
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
|
||||
$total = $node->statistics()['disk_total'] ?? 0;
|
||||
$used = $node->statistics()['disk_used'] ?? 0;
|
||||
$total = ($node->statistics()['disk_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$used = ($node->statistics()['disk_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$unused = $total - $used;
|
||||
|
||||
return [
|
||||
|
@ -10,6 +10,11 @@ class ServerResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Server::class;
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
}
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-brand-docker';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
@ -487,11 +487,12 @@ class CreateServer extends CreateRecord
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->default(0)
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@ -517,11 +518,12 @@ class CreateServer extends CreateRecord
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Space Limit')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->default(0)
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@ -551,7 +553,9 @@ class CreateServer extends CreateRecord
|
||||
->default(0)
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->helperText('100% equals one logical thread'),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@ -593,7 +597,7 @@ class CreateServer extends CreateRecord
|
||||
})
|
||||
->label('Swap Memory')
|
||||
->default(0)
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->minValue(-1)
|
||||
->columnSpan(2)
|
||||
->inlineLabel()
|
||||
@ -610,7 +614,7 @@ class CreateServer extends CreateRecord
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('oom_disabled')
|
||||
Forms\Components\ToggleButtons::make('oom_killer')
|
||||
->label('OOM Killer')
|
||||
->inlineLabel()->inline()
|
||||
->default(false)
|
||||
|
@ -244,35 +244,57 @@ class EditServer extends EditRecord
|
||||
]))
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('server_variables')
|
||||
->label('')
|
||||
->relationship('serverVariables')
|
||||
->grid()
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('variable_value')
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||
foreach ($data as $key => $value) {
|
||||
if (!isset($data['variable_value'])) {
|
||||
$data['variable_value'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
})
|
||||
->reorderable(false)->addable(false)->deletable(false)
|
||||
->schema(function () {
|
||||
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->maxLength(191)
|
||||
->rules([
|
||||
fn (ServerVariable $variable): Closure => function (string $attribute, $value, Closure $fail) use ($variable) {
|
||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $variable->variable->rules,
|
||||
'validatorkey' => $serverVariable->variable->rules,
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $variable->variable->name);
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
])
|
||||
->label(fn (ServerVariable $variable) => $variable->variable->name)
|
||||
->hintIcon('tabler-code')
|
||||
->hintIconTooltip(fn (ServerVariable $variable) => $variable->variable->rules)
|
||||
->prefix(fn (ServerVariable $variable) => '{{' . $variable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $variable) => $variable->variable->description ?: '—')
|
||||
->maxLength(191),
|
||||
]);
|
||||
|
||||
Forms\Components\Hidden::make('variable_id'),
|
||||
])
|
||||
$select = Forms\Components\Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
|
||||
$components = [$text, $select];
|
||||
|
||||
/** @var Forms\Components\Component $component */
|
||||
foreach ($components as &$component) {
|
||||
$component = $component
|
||||
->live(onBlur: true)
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules)
|
||||
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||
}
|
||||
|
||||
return $components;
|
||||
})
|
||||
->columnSpan(2),
|
||||
]),
|
||||
|
||||
@ -311,10 +333,11 @@ class EditServer extends EditRecord
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@ -340,10 +363,11 @@ class EditServer extends EditRecord
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Space Limit')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@ -372,7 +396,8 @@ class EditServer extends EditRecord
|
||||
->suffix('%')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@ -417,7 +442,7 @@ class EditServer extends EditRecord
|
||||
'limited', false => false,
|
||||
})
|
||||
->label('Swap Memory')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->minValue(-1)
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
@ -432,7 +457,7 @@ class EditServer extends EditRecord
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('oom_disabled')
|
||||
Forms\Components\ToggleButtons::make('oom_killer')
|
||||
->label('OOM Killer')->inlineLabel()->inline()
|
||||
->columnSpan(2)
|
||||
->options([
|
||||
@ -487,13 +512,6 @@ class EditServer extends EditRecord
|
||||
->color('danger')
|
||||
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
||||
->requiresConfirmation(),
|
||||
Actions\DeleteAction::make('Force Delete')
|
||||
->label('Force Delete')
|
||||
->hidden()
|
||||
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||
->color('danger')
|
||||
->after(fn (Server $server) => resolve(ServerDeletionService::class)->withForce()->handle($server))
|
||||
->requiresConfirmation(),
|
||||
Actions\Action::make('console')
|
||||
->label('Console')
|
||||
->icon('tabler-terminal')
|
||||
@ -520,4 +538,35 @@ class EditServer extends EditRecord
|
||||
ServerResource\RelationManagers\AllocationsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||
{
|
||||
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||
);
|
||||
|
||||
if ($component instanceof Forms\Components\Select) {
|
||||
return $containsRuleIn;
|
||||
}
|
||||
|
||||
if ($component instanceof Forms\Components\TextInput) {
|
||||
return !$containsRuleIn;
|
||||
}
|
||||
|
||||
throw new \Exception('Component type not supported: ' . $component::class);
|
||||
}
|
||||
|
||||
private function getSelectOptionsFromRules(Forms\Get $get): array
|
||||
{
|
||||
$inRule = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||
);
|
||||
|
||||
return str($inRule)
|
||||
->after('in:')
|
||||
->explode(',')
|
||||
->each(fn ($value) => str($value)->trim())
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
// ->actions
|
||||
// ->groups
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('ip_alias')->label('Alias'),
|
||||
Tables\Columns\TextInputColumn::make('ip_alias')->label('Alias'),
|
||||
Tables\Columns\TextColumn::make('ip')->label('IP'),
|
||||
Tables\Columns\TextColumn::make('port')->label('Port'),
|
||||
Tables\Columns\IconColumn::make('primary')
|
||||
@ -56,8 +56,8 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||
//Tables\Actions\AssociateAction::make()->label('Add Allocation'),
|
||||
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||
//TODO Tables\Actions\AssociateAction::make()->label('Add Allocation'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
|
@ -11,6 +11,10 @@ class UserResource extends Resource
|
||||
{
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count();
|
||||
}
|
||||
protected static ?string $navigationIcon = 'tabler-users';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'username';
|
||||
|
@ -67,6 +67,12 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,12 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\Servers;
|
||||
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Node;
|
||||
use Carbon\CarbonImmutable;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Lcobucci\JWT\Token\Plain;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use App\Models\ServerTransfer;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Nodes\NodeJWTService;
|
||||
use App\Models\Server;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
|
||||
class ServerTransferController extends Controller
|
||||
{
|
||||
@ -25,30 +16,10 @@ class ServerTransferController extends Controller
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private ConnectionInterface $connection,
|
||||
private NodeJWTService $nodeJWTService,
|
||||
private TransferServerService $transferServerService,
|
||||
) {
|
||||
}
|
||||
|
||||
private function notify(Server $server, Plain $token): void
|
||||
{
|
||||
try {
|
||||
Http::daemon($server->node)->post('/api/transfer', [
|
||||
'json' => [
|
||||
'server_id' => $server->uuid,
|
||||
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
|
||||
'token' => 'Bearer ' . $token->toString(),
|
||||
'server' => [
|
||||
'uuid' => $server->uuid,
|
||||
'start_on_completion' => false,
|
||||
],
|
||||
],
|
||||
])->toPsrResponse();
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a transfer of a server to a new node.
|
||||
*
|
||||
@ -62,85 +33,12 @@ class ServerTransferController extends Controller
|
||||
'allocation_additional' => 'nullable',
|
||||
]);
|
||||
|
||||
$node_id = $validatedData['node_id'];
|
||||
$allocation_id = intval($validatedData['allocation_id']);
|
||||
$additional_allocations = array_map('intval', $validatedData['allocation_additional'] ?? []);
|
||||
|
||||
// Check if the node is viable for the transfer.
|
||||
$node = Node::query()
|
||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
|
||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('nodes.id', $node_id)
|
||||
->first();
|
||||
|
||||
if (!$node->isViable($server->memory, $server->disk)) {
|
||||
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
|
||||
} else {
|
||||
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
|
||||
|
||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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([
|
||||
'allocation_id', 'add_allocations', 'remove_allocations',
|
||||
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
|
||||
'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled',
|
||||
'database_limit', 'allocation_limit', 'backup_limit', 'oom_killer',
|
||||
]));
|
||||
} catch (DataValidationException $exception) {
|
||||
throw new ValidationException($exception->getValidator());
|
||||
|
@ -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;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Server;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class ServerManagementController extends ApplicationApiController
|
||||
{
|
||||
@ -16,7 +18,9 @@ class ServerManagementController extends ApplicationApiController
|
||||
*/
|
||||
public function __construct(
|
||||
private ReinstallServerService $reinstallServerService,
|
||||
private SuspensionService $suspensionService
|
||||
private SuspensionService $suspensionService,
|
||||
private TransferServerService $transferServerService,
|
||||
private DaemonServerRepository $daemonServerRepository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@ -57,4 +61,44 @@ class ServerManagementController extends ApplicationApiController
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a transfer of a server to a new node.
|
||||
*/
|
||||
public function startTransfer(ServerWriteRequest $request, Server $server): Response
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'node_id' => 'required|exists:nodes,id',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'allocation_additional' => 'nullable',
|
||||
]);
|
||||
|
||||
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||
// Transfer started
|
||||
$this->returnNoContent();
|
||||
} else {
|
||||
// Node was not viable
|
||||
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a transfer of a server to a new node.
|
||||
*
|
||||
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function cancelTransfer(ServerWriteRequest $request, Server $server): Response
|
||||
{
|
||||
if (!$transfer = $server->transfer) {
|
||||
// Server is not transferring
|
||||
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
|
||||
}
|
||||
|
||||
$transfer->successful = true;
|
||||
$transfer->save();
|
||||
|
||||
$this->daemonServerRepository->setServer($server)->cancelTransfer();
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
}
|
||||
|
@ -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'],
|
||||
'environment' => 'present|array',
|
||||
'skip_scripts' => 'sometimes|boolean',
|
||||
'oom_disabled' => 'sometimes|boolean',
|
||||
'oom_killer' => 'sometimes|boolean',
|
||||
|
||||
// Resource limitations
|
||||
'limits' => 'required|array',
|
||||
@ -94,7 +94,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
'database_limit' => array_get($data, 'feature_limits.databases'),
|
||||
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
|
||||
'backup_limit' => array_get($data, 'feature_limits.backups'),
|
||||
'oom_disabled' => array_get($data, 'oom_disabled'),
|
||||
'oom_killer' => array_get($data, 'oom_killer'),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
||||
|
||||
return [
|
||||
'allocation' => $rules['allocation_id'],
|
||||
'oom_disabled' => $rules['oom_disabled'],
|
||||
'oom_killer' => $rules['oom_killer'],
|
||||
|
||||
'limits' => 'sometimes|array',
|
||||
'limits.memory' => $this->requiredToOptional('memory', $rules['memory'], true),
|
||||
|
@ -19,9 +19,11 @@ class StoreScheduleRequest extends ViewScheduleRequest
|
||||
return [
|
||||
'name' => $rules['name'],
|
||||
'is_active' => array_merge(['filled'], $rules['is_active']),
|
||||
'only_when_online' => $rules['only_when_online'],
|
||||
'minute' => $rules['cron_minute'],
|
||||
'hour' => $rules['cron_hour'],
|
||||
'day_of_month' => $rules['cron_day_of_month'],
|
||||
'month' => $rules['cron_month'],
|
||||
'day_of_week' => $rules['cron_day_of_week'],
|
||||
];
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
* @property int $io
|
||||
* @property int $cpu
|
||||
* @property string|null $threads
|
||||
* @property bool $oom_disabled
|
||||
* @property bool $oom_killer
|
||||
* @property int $allocation_id
|
||||
* @property int $egg_id
|
||||
* @property string $startup
|
||||
@ -90,7 +90,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereMemory($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereNodeId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomDisabled($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomKiller($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOwnerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereSkipScripts($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereStartup($value)
|
||||
@ -124,7 +124,7 @@ class Server extends Model
|
||||
*/
|
||||
protected $attributes = [
|
||||
'status' => ServerState::Installing,
|
||||
'oom_disabled' => true,
|
||||
'oom_killer' => false,
|
||||
'installed_at' => null,
|
||||
];
|
||||
|
||||
@ -150,7 +150,7 @@ class Server extends Model
|
||||
'io' => 'required|numeric|between:0,1000',
|
||||
'cpu' => 'required|numeric|min:0',
|
||||
'threads' => 'nullable|regex:/^[0-9-,]+$/',
|
||||
'oom_disabled' => 'sometimes|boolean',
|
||||
'oom_killer' => 'sometimes|boolean',
|
||||
'disk' => 'required|numeric|min:0',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'egg_id' => 'required|exists:eggs,id',
|
||||
@ -174,7 +174,7 @@ class Server extends Model
|
||||
'disk' => 'integer',
|
||||
'io' => 'integer',
|
||||
'cpu' => 'integer',
|
||||
'oom_disabled' => 'boolean',
|
||||
'oom_killer' => 'boolean',
|
||||
'allocation_id' => 'integer',
|
||||
'egg_id' => 'integer',
|
||||
'database_limit' => 'integer',
|
||||
|
@ -19,6 +19,7 @@ use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use Saade\FilamentLaravelLog\FilamentLaravelLogPlugin;
|
||||
|
||||
class AdminPanelProvider extends PanelProvider
|
||||
{
|
||||
@ -35,8 +36,8 @@ class AdminPanelProvider extends PanelProvider
|
||||
->default()
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->topNavigation(true)
|
||||
->login()
|
||||
->brandName('Pelican')
|
||||
->homeUrl('/')
|
||||
->favicon('/pelican.ico')
|
||||
->profile(EditProfile::class, false)
|
||||
@ -74,6 +75,13 @@ class AdminPanelProvider extends PanelProvider
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
])
|
||||
->plugin(
|
||||
FilamentLaravelLogPlugin::make()
|
||||
->navigationLabel('Logs')
|
||||
->navigationIcon('tabler-file-info')
|
||||
->slug('logs')
|
||||
->authorize(fn () => auth()->user()->root_admin)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +141,46 @@ class DaemonServerRepository extends DaemonRepository
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a server transfer.
|
||||
*
|
||||
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function cancelTransfer(): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
if ($transfer = $this->server->transfer) {
|
||||
// Source node
|
||||
$this->setNode($transfer->oldNode);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->delete(sprintf(
|
||||
'/api/servers/%s/transfer',
|
||||
$this->server->uuid
|
||||
));
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
// Destination node
|
||||
$this->setNode($transfer->newNode);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->delete('/api/transfer', [
|
||||
'json' => [
|
||||
'server_id' => $this->server->uuid,
|
||||
'server' => [
|
||||
'uuid' => $this->server->uuid,
|
||||
],
|
||||
],
|
||||
]);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes a single user's JTI by using their ID. This is simply a helper function to
|
||||
* make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted
|
||||
|
@ -40,8 +40,12 @@ class BuildModificationService
|
||||
throw_unless($existingAllocation, new DisplayException('The requested default allocation is not currently assigned to this server.'));
|
||||
}
|
||||
|
||||
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
|
||||
$data['oom_killer'] = !$data['oom_disabled'];
|
||||
}
|
||||
|
||||
// If any of these values are passed through in the data array go ahead and set them correctly on the server model.
|
||||
$merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
|
||||
$merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
|
||||
|
||||
$server->forceFill(array_merge($merge, [
|
||||
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
|
||||
|
@ -59,14 +59,12 @@ class ServerConfigurationStructureService
|
||||
'cpu_limit' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'disk_space' => $server->disk,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
// This field is deprecated — use "oom_killer".
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'oom_killer' => $server->oom_killer,
|
||||
],
|
||||
'container' => [
|
||||
'image' => $server->image,
|
||||
// This field is deprecated — use the value in the "build" block.
|
||||
//
|
||||
// TODO: remove this key in V2.
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
'requires_rebuild' => false,
|
||||
],
|
||||
'allocations' => [
|
||||
@ -110,7 +108,7 @@ class ServerConfigurationStructureService
|
||||
return $item->pluck('port');
|
||||
})->toArray(),
|
||||
'env' => $this->environment->handle($server),
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'memory' => (int) $server->memory,
|
||||
'swap' => (int) $server->swap,
|
||||
'io' => (int) $server->io,
|
||||
|
@ -47,6 +47,10 @@ class ServerCreationService
|
||||
*/
|
||||
public function handle(array $data, DeploymentObject $deployment = null): Server
|
||||
{
|
||||
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
|
||||
$data['oom_killer'] = !$data['oom_disabled'];
|
||||
}
|
||||
|
||||
// If a deployment object has been passed we need to get the allocation
|
||||
// that the server should use, and assign the node from that allocation.
|
||||
if ($deployment instanceof DeploymentObject) {
|
||||
@ -142,7 +146,7 @@ class ServerCreationService
|
||||
'io' => Arr::get($data, 'io'),
|
||||
'cpu' => Arr::get($data, 'cpu'),
|
||||
'threads' => Arr::get($data, 'threads'),
|
||||
'oom_disabled' => Arr::get($data, 'oom_disabled') ?? true,
|
||||
'oom_killer' => Arr::get($data, 'oom_killer') ?? false,
|
||||
'allocation_id' => Arr::get($data, 'allocation_id'),
|
||||
'egg_id' => Arr::get($data, 'egg_id'),
|
||||
'startup' => Arr::get($data, 'startup'),
|
||||
|
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;
|
||||
|
||||
use App\Models\Node;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use League\Fractal\Resource\Item;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
@ -12,6 +14,7 @@ class DatabaseHostTransformer extends BaseTransformer
|
||||
{
|
||||
protected array $availableIncludes = [
|
||||
'databases',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -54,4 +57,20 @@ class DatabaseHostTransformer extends BaseTransformer
|
||||
|
||||
return $this->collection($model->getRelation('databases'), $this->makeTransformer(ServerDatabaseTransformer::class), Database::RESOURCE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the node associated with this host.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeNode(DatabaseHost $model): Item|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$model->loadMissing('node');
|
||||
|
||||
return $this->item($model->getRelation('node'), $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME);
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,9 @@ class ServerTransformer extends BaseTransformer
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
// This field is deprecated, please use "oom_killer".
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'oom_killer' => $server->oom_killer,
|
||||
],
|
||||
'feature_limits' => [
|
||||
'databases' => $server->database_limit,
|
||||
|
@ -56,7 +56,9 @@ class ServerTransformer extends BaseClientTransformer
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
// This field is deprecated, please use "oom_killer".
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'oom_killer' => $server->oom_killer,
|
||||
],
|
||||
'invocation' => $service->handle($server, !$user->can(Permission::ACTION_STARTUP_READ, $server)),
|
||||
'docker_image' => $server->image,
|
||||
|
@ -32,6 +32,7 @@
|
||||
"prologue/alerts": "^1.2",
|
||||
"ryangjchandler/blade-tabler-icons": "^2.3",
|
||||
"s1lentium/iptools": "~1.2.0",
|
||||
"saade/filament-laravel-log": "^3.2",
|
||||
"spatie/laravel-fractal": "^6.1",
|
||||
"spatie/laravel-query-builder": "^5.8",
|
||||
"symfony/mailgun-mailer": "^7.0",
|
||||
|
761
composer.lock
generated
761
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ use Illuminate\Support\Facades\Facade;
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Pelican',
|
||||
'name' => env('APP_NAME', 'Pelican'),
|
||||
|
||||
'version' => 'canary',
|
||||
|
||||
|
@ -46,6 +46,8 @@ return [
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'client' => env('REDIS_CLIENT', 'predis'),
|
||||
|
||||
'default' => [
|
||||
'scheme' => env('REDIS_SCHEME', 'tcp'),
|
||||
'path' => env('REDIS_PATH', '/run/redis/redis.sock'),
|
||||
|
@ -35,7 +35,7 @@ class ServerFactory extends Factory
|
||||
'io' => 500,
|
||||
'cpu' => 0,
|
||||
'threads' => null,
|
||||
'oom_disabled' => 0,
|
||||
'oom_killer' => false,
|
||||
'startup' => '/bin/bash echo "hello world"',
|
||||
'image' => 'foo/bar:latest',
|
||||
'allocation_limit' => null,
|
||||
|
@ -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,
|
||||
`cpu` int unsigned NOT NULL,
|
||||
`threads` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`oom_disabled` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`oom_killer` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`allocation_id` int unsigned NOT NULL,
|
||||
`egg_id` int unsigned NOT NULL,
|
||||
`startup` text COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
@ -844,3 +844,4 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (197,'2024_03_12_15
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (198,'2024_03_14_055537_remove_locations_table',2);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (201,'2024_04_20_214441_add_egg_var_sort',3);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (203,'2024_04_14_002250_update_column_names',4);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (204,'2024_05_08_094823_rename_oom_disabled_column_to_oom_killer',1);
|
||||
|
@ -17,7 +17,6 @@
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="MAIL_DRIVER" value="array"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
|
@ -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
|
||||
|
||||
|
@ -203,8 +203,8 @@
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<div class="checkbox checkbox-primary no-margin-bottom">
|
||||
<input type="checkbox" id="pOomDisabled" name="oom_disabled" value="0" {{ \App\Helpers\Utilities::checked('oom_disabled', 0) }} />
|
||||
<label for="pOomDisabled" class="strong">Enable OOM Killer</label>
|
||||
<input type="checkbox" id="pOomKiller" name="oom_killer" value="0" {{ \App\Helpers\Utilities::checked('oom_killer', 0) }} />
|
||||
<label for="pOomKiller" class="strong">Enable OOM Killer</label>
|
||||
</div>
|
||||
|
||||
<p class="small text-muted no-margin">Terminates the server if it breaches the memory limits. Enabling OOM killer may cause server processes to exit unexpectedly.</p>
|
||||
|
@ -74,11 +74,11 @@
|
||||
<label for="cpu" class="control-label">OOM Killer</label>
|
||||
<div>
|
||||
<div class="radio radio-danger radio-inline">
|
||||
<input type="radio" id="pOomKillerEnabled" value="0" name="oom_disabled" @if(!$server->oom_disabled)checked @endif>
|
||||
<input type="radio" id="pOomKillerEnabled" value="1" name="oom_killer" @if(!$server->oom_killer)checked @endif>
|
||||
<label for="pOomKillerEnabled">Enabled</label>
|
||||
</div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pOomKillerDisabled" value="1" name="oom_disabled" @if($server->oom_disabled)checked @endif>
|
||||
<input type="radio" id="pOomKillerDisabled" value="0" name="oom_killer" @if($server->oom_killer)checked @endif>
|
||||
<label for="pOomKillerDisabled">Disabled</label>
|
||||
</div>
|
||||
<p class="text-muted small">
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="{{ config('scramble.theme', 'dark') }}">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
@ -69,6 +69,8 @@ Route::prefix('/servers')->group(function () {
|
||||
Route::post('/{server:id}/suspend', [Application\Servers\ServerManagementController::class, 'suspend'])->name('api.application.servers.suspend');
|
||||
Route::post('/{server:id}/unsuspend', [Application\Servers\ServerManagementController::class, 'unsuspend'])->name('api.application.servers.unsuspend');
|
||||
Route::post('/{server:id}/reinstall', [Application\Servers\ServerManagementController::class, 'reinstall'])->name('api.application.servers.reinstall');
|
||||
Route::post('/{server:id}/transfer', [Application\Servers\ServerManagementController::class, 'startTransfer'])->name('api.application.servers.transfer');
|
||||
Route::post('/{server:id}/transfer/cancel', [Application\Servers\ServerManagementController::class, 'cancelTransfer'])->name('api.application.servers.transfer.cancel');
|
||||
|
||||
Route::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']);
|
||||
Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']);
|
||||
@ -97,3 +99,22 @@ Route::prefix('/eggs')->group(function () {
|
||||
Route::get('/', [Application\Eggs\EggController::class, 'index'])->name('api.application.eggs.eggs');
|
||||
Route::get('/{egg:id}', [Application\Eggs\EggController::class, 'view'])->name('api.application.eggs.eggs.view');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Host Controller Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoint: /api/application/database-hosts
|
||||
|
|
||||
*/
|
||||
Route::group(['prefix' => '/database-hosts'], function () {
|
||||
Route::get('/', [Application\DatabaseHosts\DatabaseHostController::class, 'index'])->name('api.application.databasehosts');
|
||||
Route::get('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'view'])->name('api.application.databasehosts.view');
|
||||
|
||||
Route::post('/', [Application\DatabaseHosts\DatabaseHostController::class, 'store']);
|
||||
|
||||
Route::patch('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'update']);
|
||||
|
||||
Route::delete('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'delete']);
|
||||
});
|
||||
|
@ -57,7 +57,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase
|
||||
$response = $this->actingAs($user)->postJson("/api/client/servers/$server->uuid/schedules", []);
|
||||
|
||||
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
foreach (['name', 'minute', 'hour', 'day_of_month', 'day_of_week'] as $i => $field) {
|
||||
foreach (['name', 'minute', 'hour', 'day_of_month', 'month', 'day_of_week'] as $i => $field) {
|
||||
$response->assertJsonPath("errors.$i.code", 'ValidationException');
|
||||
$response->assertJsonPath("errors.$i.meta.rule", 'required');
|
||||
$response->assertJsonPath("errors.$i.meta.source_field", $field);
|
||||
@ -67,6 +67,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase
|
||||
->postJson("/api/client/servers/$server->uuid/schedules", [
|
||||
'name' => 'Testing',
|
||||
'is_active' => 'no',
|
||||
'only_when_online' => 'false',
|
||||
'minute' => '*',
|
||||
'hour' => '*',
|
||||
'day_of_month' => '*',
|
||||
|
@ -114,7 +114,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
$this->daemonServerRepository->expects('sync')->withNoArgs()->andReturnUndefined();
|
||||
|
||||
$response = $this->getService()->handle($server, [
|
||||
'oom_disabled' => false,
|
||||
'oom_killer' => false,
|
||||
'memory' => 256,
|
||||
'swap' => 128,
|
||||
'io' => 600,
|
||||
@ -126,7 +126,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
'allocation_limit' => 20,
|
||||
]);
|
||||
|
||||
$this->assertFalse($response->oom_disabled);
|
||||
$this->assertFalse($response->oom_killer);
|
||||
$this->assertSame(256, $response->memory);
|
||||
$this->assertSame(128, $response->swap);
|
||||
$this->assertSame(600, $response->io);
|
||||
|
@ -138,7 +138,7 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
||||
$this->assertSame($allocations[4]->id, $response->allocations[1]->id);
|
||||
|
||||
$this->assertFalse($response->isSuspended());
|
||||
$this->assertTrue($response->oom_disabled);
|
||||
$this->assertFalse($response->oom_killer);
|
||||
$this->assertSame(0, $response->database_limit);
|
||||
$this->assertSame(0, $response->allocation_limit);
|
||||
$this->assertSame(0, $response->backup_limit);
|
||||
|
Loading…
x
Reference in New Issue
Block a user