Update nodes

This commit is contained in:
Lance Pioch 2024-03-24 14:42:36 -04:00
parent 2664ba0774
commit 039ac40cf7
4 changed files with 257 additions and 67 deletions

View File

@ -3,15 +3,12 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\NodeResource\Pages; use App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource\RelationManagers;
use App\Models\Node; use App\Models\Node;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class NodeResource extends Resource class NodeResource extends Resource
{ {
@ -25,31 +22,8 @@ class NodeResource extends Resource
{ {
return $form return $form
->schema([ ->schema([
Forms\Components\TextInput::make('uuid')
->label('UUID')
->required()
->maxLength(36),
Forms\Components\TextInput::make('public')
->required()
->numeric(),
Forms\Components\TextInput::make('name')
->required()
->maxLength(191),
Forms\Components\Textarea::make('description')
->columnSpanFull(),
Forms\Components\TextInput::make('location_id')
->required()
->numeric(),
Forms\Components\TextInput::make('fqdn')
->required()
->maxLength(191),
Forms\Components\TextInput::make('scheme')
->required()
->maxLength(191)
->default('https'),
Forms\Components\Toggle::make('behind_proxy') Forms\Components\Toggle::make('behind_proxy')
->required(), ->helperText('If you are running the daemon behind a proxy such as Cloudflare, select this to have the daemon skip looking for certificates on boot.')
Forms\Components\Toggle::make('maintenance_mode')
->required(), ->required(),
Forms\Components\TextInput::make('memory') Forms\Components\TextInput::make('memory')
->required() ->required()
@ -67,23 +41,40 @@ class NodeResource extends Resource
->default(0), ->default(0),
Forms\Components\TextInput::make('upload_size') Forms\Components\TextInput::make('upload_size')
->required() ->required()
->numeric() ->integer()
->default(100), ->default(100),
Forms\Components\TextInput::make('daemon_token_id')
->required()
->maxLength(16),
Forms\Components\TextInput::make('daemonListen') Forms\Components\TextInput::make('daemonListen')
->required() ->required()
->numeric() ->integer()
->label('Daemon Port')
->default(8080), ->default(8080),
Forms\Components\TextInput::make('daemonSFTP') Forms\Components\TextInput::make('daemonSFTP')
->required() ->required()
->numeric() ->integer()
->label('Daemon SFTP Port')
->default(2022), ->default(2022),
Forms\Components\TextInput::make('daemonBase') Forms\Components\TextInput::make('daemonBase')
->required() ->required()
->maxLength(191) ->maxLength(191)
->default('/home/daemon-files'), ->default('/home/daemon-files'),
Forms\Components\ToggleButtons::make('public')
->label('Node Visibility')
->inline()
->default(true)
->helperText('By setting a node to private you will be denying the ability to auto-deploy to this node.')
->options([
true => 'Public',
false => 'Private',
])
->colors([
true => 'warning',
false => 'danger',
])
->icons([
true => 'heroicon-m-eye',
false => 'heroicon-m-lock-closed',
]),
]); ]);
} }
@ -93,48 +84,39 @@ class NodeResource extends Resource
->columns([ ->columns([
Tables\Columns\TextColumn::make('uuid') Tables\Columns\TextColumn::make('uuid')
->label('UUID') ->label('UUID')
->searchable(), ->searchable()
Tables\Columns\TextColumn::make('public') ->hidden(),
->numeric() Tables\Columns\IconColumn::make('health')
->sortable(), ->alignCenter()
->state(fn (Node $node) => $node->systemInformation()['version'] ?? false)
->tooltip(fn (Node $node) => $node->systemInformation()['version'] ?? $node->systemInformation()['exception'] ?? 'Not Connected')
->trueIcon('heroicon-m-heart')
->default(false),
Tables\Columns\TextColumn::make('name') Tables\Columns\TextColumn::make('name')
->searchable(), ->searchable(),
Tables\Columns\TextColumn::make('location_id')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('fqdn') Tables\Columns\TextColumn::make('fqdn')
->searchable(), ->searchable(),
Tables\Columns\TextColumn::make('scheme')
->searchable(),
Tables\Columns\IconColumn::make('behind_proxy')
->boolean(),
Tables\Columns\IconColumn::make('maintenance_mode')
->boolean(),
Tables\Columns\TextColumn::make('memory') Tables\Columns\TextColumn::make('memory')
->numeric() ->numeric()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('memory_overallocate')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('disk') Tables\Columns\TextColumn::make('disk')
->numeric() ->numeric()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('disk_overallocate')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('upload_size')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('daemon_token_id')
->searchable(),
Tables\Columns\TextColumn::make('daemonListen')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('daemonSFTP')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('daemonBase') Tables\Columns\TextColumn::make('daemonBase')
->searchable(), ->searchable(),
Tables\Columns\IconColumn::make('scheme')
->label('SSL')
->trueIcon('heroicon-m-lock-closed')
->falseIcon('heroicon-m-lock-open')
->state(fn (Node $node) => $node->scheme === 'https'),
Tables\Columns\IconColumn::make('public')
->trueIcon('heroicon-m-eye')
->falseIcon('heroicon-m-eye-slash')
->sortable(),
Tables\Columns\TextColumn::make('servers_count')
->counts('servers')
->label('Servers')
->icon('heroicon-m-server-stack'),
Tables\Columns\TextColumn::make('created_at') Tables\Columns\TextColumn::make('created_at')
->dateTime() ->dateTime()
->sortable() ->sortable()

View File

@ -3,10 +3,142 @@
namespace App\Filament\Resources\NodeResource\Pages; namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource; use App\Filament\Resources\NodeResource;
use Filament\Actions; use Filament\Forms;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
class CreateNode extends CreateRecord class CreateNode extends CreateRecord
{ {
protected static string $resource = NodeResource::class; protected static string $resource = NodeResource::class;
public function form(Forms\Form $form): Forms\Form
{
return $form
->columns(2)
->schema([
Forms\Components\TextInput::make('fqdn')
->label('Domain Name')
->placeholder('node.example.com')
->helperText('Node\'s Domain Name')
->required()
->autofocus()
->columns(3)
->live(debounce: 500)
->hidden(fn (Forms\Get $get) => !$get('isHostname'))
->disabled(fn (Forms\Get $get) => !$get('isHostname'))
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
$hasRecords = checkdnsrr("$state.", 'A');
if (!$hasRecords) {
Notification::make()
->title('Your hostname does not appear to have a valid A record.')
->warning()
->send();
}
})
->maxLength(191),
Forms\Components\TextInput::make('fqdn')
->label('IP Address')
->placeholder('127.0.0.1')
->helperText('Node\'s IP Address')
->required()
->ipv4()
->columns(3)
->live(debounce: 500)
->hidden(fn (Forms\Get $get) => $get('isHostname'))
->disabled(fn (Forms\Get $get) => $get('isHostname'))
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
$isIp = filter_var($state, FILTER_VALIDATE_IP) !== false;
$isSecure = request()->isSecure();
if ($isIp && $isSecure) {
Notification::make()
->title('You cannot use an IP Address because you have a secure connection to the panel currently.')
->danger()
->send();
$set('name', $state);
}
})
->maxLength(191),
Forms\Components\ToggleButtons::make('isHostname')
->label('Address Type')
->options([
true => 'Hostname',
false => 'IP Address',
])
->inline()
->live()
->afterStateUpdated(function () {
})
->default(true),
Forms\Components\TextInput::make('daemonListen')
->columns(1)
->label('Port')
->helperText('If you will be 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\ToggleButtons::make('scheme')
->label('Communicate over SSL')
->required()
->dehydrated()
->inline()
// request()->isSecure()
->helperText(function (Forms\Get $get) {
if (request()->isSecure()) {
return 'Your Panel is currently using secure connection therefore so must your Daemon.
This automatically disables using an IP Address for a FQDN.';
}
if (filter_var($get('fqdn'), FILTER_VALIDATE_IP) !== false) {
return 'An IP address cannot use SSL.';
}
return '';
})
// ->helperText(fn (Forms\Get $get) => filter_var($get('fqdn'), FILTER_VALIDATE_IP) !== false ? 'An IP address cannot use SSL.' : '')
->disabled(function (Forms\Get $get, Forms\Set $set) {
$isIp = filter_var($get('fqdn'), FILTER_VALIDATE_IP) !== false;
$isSecure = request()->isSecure();
if ($isSecure) {
$set('scheme', 'https');
return true;
}
if ($isIp) {
$set('scheme', 'http');
return true;
}
})
->options([
'http' => 'HTTP',
'https' => 'SSL (HTTPS)',
])
->colors([
'http' => 'warning',
'https' => 'success',
])
->icons([
'http' => 'heroicon-m-lock-open',
'https' => 'heroicon-m-lock-closed',
])
->default('http'),
Forms\Components\TextInput::make('name')
->required()
->columnSpanFull()
->regex('/[a-zA-Z0-9_\.\- ]+/')
->helperText('Character limits: [a-zA-Z0-9_.-] and [Space]')
->maxLength(100),
Forms\Components\Textarea::make('description')->columnSpanFull()->rows(5),
]);
}
} }

View File

@ -4,12 +4,44 @@ namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource; use App\Filament\Resources\NodeResource;
use Filament\Actions; use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Wizard;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditNode extends EditRecord class EditNode extends EditRecord
{ {
protected static string $resource = NodeResource::class; protected static string $resource = NodeResource::class;
public function form(Forms\Form $form): Forms\Form
{
return $form
->schema([
Wizard::make([
Forms\Components\Wizard\Step::make('Basic')
->description('')
->schema((new CreateNode())->form($form)->getComponents()),
Forms\Components\Wizard\Step::make('Configuration')
->description('')
->schema([
]),
])
->columns(4)
->persistStepInQueryString()
->columnSpanFull()
// ->startOnStep($this->getStartStep())
// ->cancelAction($this->getCancelFormAction())
// ->submitAction($this->getSubmitFormAction())
// ->skippable($this->hasSkippableSteps()),
]);
}
protected function getSteps(): array
{
return [
];
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [

View File

@ -2,6 +2,8 @@
namespace App\Models; namespace App\Models;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use Exception;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@ -79,9 +81,9 @@ class Node extends Model
'fqdn' => 'required|string', 'fqdn' => 'required|string',
'scheme' => 'required', 'scheme' => 'required',
'behind_proxy' => 'boolean', 'behind_proxy' => 'boolean',
'memory' => 'required|numeric|min:1', 'memory' => 'required|numeric|min:0',
'memory_overallocate' => 'required|numeric|min:-1', 'memory_overallocate' => 'required|numeric|min:-1',
'disk' => 'required|numeric|min:1', 'disk' => 'required|numeric|min:0',
'disk_overallocate' => 'required|numeric|min:-1', 'disk_overallocate' => 'required|numeric|min:-1',
'daemonBase' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/', 'daemonBase' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
'daemonSFTP' => 'required|numeric|between:1,65535', 'daemonSFTP' => 'required|numeric|between:1,65535',
@ -96,9 +98,11 @@ class Node extends Model
protected $attributes = [ protected $attributes = [
'public' => true, 'public' => true,
'behind_proxy' => false, 'behind_proxy' => false,
'memory' => 0,
'memory_overallocate' => 0, 'memory_overallocate' => 0,
'disk' => 0,
'disk_overallocate' => 0, 'disk_overallocate' => 0,
'daemonBase' => '/var/lib/panel/volumes', 'daemonBase' => '/var/lib/pelican/volumes',
'daemonSFTP' => 2022, 'daemonSFTP' => 2022,
'daemonListen' => 8080, 'daemonListen' => 8080,
'maintenance_mode' => false, 'maintenance_mode' => false,
@ -117,6 +121,23 @@ class Node extends Model
]; ];
} }
public function getRouteKeyName(): string
{
return 'id';
}
protected static function booted(): void
{
static::creating(function (self $node) {
$node->uuid = Str::uuid();
$node->daemon_token = encrypt(Str::random(self::DAEMON_TOKEN_LENGTH));
$node->daemon_token_id = Str::random(self::DAEMON_TOKEN_ID_LENGTH);
return true;
});
}
/** /**
* Get the connection address to use when making calls to this node. * Get the connection address to use when making calls to this node.
*/ */
@ -240,4 +261,27 @@ class Node extends Model
]; ];
})->values(); })->values();
} }
public function systemInformation(): array
{
return once(function () {
try {
return resolve(DaemonConfigurationRepository::class)
->setNode($this)
->getSystemInformation(connectTimeout: 1);
} catch (Exception $exception) {
$message = str($exception->getMessage());
if ($message->startsWith('cURL error 6: Could not resolve host')) {
$message = str('Could not resolve host');
}
if ($message->startsWith('cURL error 28: Failed to connect to ')) {
$message = $message->after('cURL error 28: ')->before(' after ');
}
return ['exception' => $message->toString()];
}
});
}
} }