mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-20 04:04:45 +02:00
Add back auto deploy (#627)
* Add Docker, Refactor, Fix Notification Co-authored-by: notCharles <charles@pelican.dev> * Pint * Required adjustments * Remove deprecated * Third time's the charm --------- Co-authored-by: notCharles <charles@pelican.dev>
This commit is contained in:
parent
291b514e24
commit
f3de185508
@ -19,7 +19,7 @@ class ApiKeyResource extends Resource
|
|||||||
|
|
||||||
public static function getNavigationBadge(): ?string
|
public static function getNavigationBadge(): ?string
|
||||||
{
|
{
|
||||||
return static::getModel()::where('key_type', '2')->count() ?: null;
|
return static::getModel()::where('key_type', ApiKey::TYPE_APPLICATION)->count() ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function canEdit(Model $record): bool
|
public static function canEdit(Model $record): bool
|
||||||
|
@ -4,9 +4,11 @@ namespace App\Filament\Resources\NodeResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Resources\NodeResource;
|
use App\Filament\Resources\NodeResource;
|
||||||
use App\Models\Node;
|
use App\Models\Node;
|
||||||
|
use App\Services\Nodes\NodeAutoDeployService;
|
||||||
use App\Services\Nodes\NodeUpdateService;
|
use App\Services\Nodes\NodeUpdateService;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Actions as FormActions;
|
||||||
use Filament\Forms\Components\Fieldset;
|
use Filament\Forms\Components\Fieldset;
|
||||||
use Filament\Forms\Components\Grid;
|
use Filament\Forms\Components\Grid;
|
||||||
use Filament\Forms\Components\Placeholder;
|
use Filament\Forms\Components\Placeholder;
|
||||||
@ -21,6 +23,7 @@ use Filament\Forms\Get;
|
|||||||
use Filament\Forms\Set;
|
use Filament\Forms\Set;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Filament\Support\Enums\Alignment;
|
||||||
use Illuminate\Support\HtmlString;
|
use Illuminate\Support\HtmlString;
|
||||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||||
|
|
||||||
@ -149,19 +152,9 @@ class EditNode extends EditRecord
|
|||||||
true => 'success',
|
true => 'success',
|
||||||
false => 'danger',
|
false => 'danger',
|
||||||
])
|
])
|
||||||
->columnSpan([
|
->columnSpan(1),
|
||||||
'default' => 1,
|
|
||||||
'sm' => 1,
|
|
||||||
'md' => 1,
|
|
||||||
'lg' => 1,
|
|
||||||
]),
|
|
||||||
TextInput::make('daemon_listen')
|
TextInput::make('daemon_listen')
|
||||||
->columnSpan([
|
->columnSpan(1)
|
||||||
'default' => 1,
|
|
||||||
'sm' => 1,
|
|
||||||
'md' => 1,
|
|
||||||
'lg' => 1,
|
|
||||||
])
|
|
||||||
->label(trans('strings.port'))
|
->label(trans('strings.port'))
|
||||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||||
->minValue(1)
|
->minValue(1)
|
||||||
@ -182,12 +175,7 @@ class EditNode extends EditRecord
|
|||||||
->maxLength(100),
|
->maxLength(100),
|
||||||
ToggleButtons::make('scheme')
|
ToggleButtons::make('scheme')
|
||||||
->label('Communicate over SSL')
|
->label('Communicate over SSL')
|
||||||
->columnSpan([
|
->columnSpan(1)
|
||||||
'default' => 1,
|
|
||||||
'sm' => 1,
|
|
||||||
'md' => 1,
|
|
||||||
'lg' => 1,
|
|
||||||
])
|
|
||||||
->inline()
|
->inline()
|
||||||
->helperText(function (Get $get) {
|
->helperText(function (Get $get) {
|
||||||
if (request()->isSecure()) {
|
if (request()->isSecure()) {
|
||||||
@ -215,23 +203,48 @@ class EditNode extends EditRecord
|
|||||||
])
|
])
|
||||||
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
||||||
Tab::make('Advanced Settings')
|
Tab::make('Advanced Settings')
|
||||||
->columns(['default' => 1, 'sm' => 1, 'md' => 4, 'lg' => 6])
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
->icon('tabler-server-cog')
|
->icon('tabler-server-cog')
|
||||||
->schema([
|
->schema([
|
||||||
TextInput::make('id')
|
TextInput::make('id')
|
||||||
->label('Node ID')
|
->label('Node ID')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 1,
|
||||||
|
])
|
||||||
->disabled(),
|
->disabled(),
|
||||||
TextInput::make('uuid')
|
TextInput::make('uuid')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 2,
|
||||||
|
])
|
||||||
->label('Node UUID')
|
->label('Node UUID')
|
||||||
->hintAction(CopyAction::make())
|
->hintAction(CopyAction::make())
|
||||||
->disabled(),
|
->disabled(),
|
||||||
TagsInput::make('tags')
|
TagsInput::make('tags')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 2,
|
||||||
|
])
|
||||||
->placeholder('Add Tags'),
|
->placeholder('Add Tags'),
|
||||||
TextInput::make('upload_size')
|
TextInput::make('upload_size')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 1,
|
||||||
|
])
|
||||||
->label('Upload Limit')
|
->label('Upload Limit')
|
||||||
->hintIcon('tabler-question-mark')
|
->hintIcon('tabler-question-mark')
|
||||||
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||||
@ -240,7 +253,12 @@ class EditNode extends EditRecord
|
|||||||
->maxValue(1024)
|
->maxValue(1024)
|
||||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
||||||
TextInput::make('daemon_sftp')
|
TextInput::make('daemon_sftp')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
->label('SFTP Port')
|
->label('SFTP Port')
|
||||||
->minValue(1)
|
->minValue(1)
|
||||||
->maxValue(65535)
|
->maxValue(65535)
|
||||||
@ -248,11 +266,21 @@ class EditNode extends EditRecord
|
|||||||
->required()
|
->required()
|
||||||
->integer(),
|
->integer(),
|
||||||
TextInput::make('daemon_sftp_alias')
|
TextInput::make('daemon_sftp_alias')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
->label('SFTP Alias')
|
->label('SFTP Alias')
|
||||||
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
|
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
|
||||||
ToggleButtons::make('public')
|
ToggleButtons::make('public')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
->label('Use Node for deployment?')->inline()
|
->label('Use Node for deployment?')->inline()
|
||||||
->options([
|
->options([
|
||||||
true => 'Yes',
|
true => 'Yes',
|
||||||
@ -263,7 +291,12 @@ class EditNode extends EditRecord
|
|||||||
false => 'danger',
|
false => 'danger',
|
||||||
]),
|
]),
|
||||||
ToggleButtons::make('maintenance_mode')
|
ToggleButtons::make('maintenance_mode')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
->label('Maintenance Mode')->inline()
|
->label('Maintenance Mode')->inline()
|
||||||
->hinticon('tabler-question-mark')
|
->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.")
|
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
|
||||||
@ -276,7 +309,12 @@ class EditNode extends EditRecord
|
|||||||
true => 'danger',
|
true => 'danger',
|
||||||
]),
|
]),
|
||||||
Grid::make()
|
Grid::make()
|
||||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->schema([
|
->schema([
|
||||||
ToggleButtons::make('unlimited_mem')
|
ToggleButtons::make('unlimited_mem')
|
||||||
@ -293,14 +331,24 @@ class EditNode extends EditRecord
|
|||||||
true => 'primary',
|
true => 'primary',
|
||||||
false => 'warning',
|
false => 'warning',
|
||||||
])
|
])
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
]),
|
||||||
TextInput::make('memory')
|
TextInput::make('memory')
|
||||||
->dehydratedWhenHidden()
|
->dehydratedWhenHidden()
|
||||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||||
->label('Memory Limit')->inlineLabel()
|
->label('Memory Limit')->inlineLabel()
|
||||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||||
->required()
|
->required()
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
])
|
||||||
->numeric()
|
->numeric()
|
||||||
->minValue(0),
|
->minValue(0),
|
||||||
TextInput::make('memory_overallocate')
|
TextInput::make('memory_overallocate')
|
||||||
@ -310,14 +358,24 @@ class EditNode extends EditRecord
|
|||||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||||
->hintIcon('tabler-question-mark')
|
->hintIcon('tabler-question-mark')
|
||||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
])
|
||||||
->numeric()
|
->numeric()
|
||||||
->minValue(-1)
|
->minValue(-1)
|
||||||
->maxValue(100)
|
->maxValue(100)
|
||||||
->suffix('%'),
|
->suffix('%'),
|
||||||
]),
|
]),
|
||||||
Grid::make()
|
Grid::make()
|
||||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
->schema([
|
->schema([
|
||||||
ToggleButtons::make('unlimited_disk')
|
ToggleButtons::make('unlimited_disk')
|
||||||
->label('Disk')->inlineLabel()->inline()
|
->label('Disk')->inlineLabel()->inline()
|
||||||
@ -333,14 +391,24 @@ class EditNode extends EditRecord
|
|||||||
true => 'primary',
|
true => 'primary',
|
||||||
false => 'warning',
|
false => 'warning',
|
||||||
])
|
])
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
]),
|
||||||
TextInput::make('disk')
|
TextInput::make('disk')
|
||||||
->dehydratedWhenHidden()
|
->dehydratedWhenHidden()
|
||||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||||
->label('Disk Limit')->inlineLabel()
|
->label('Disk Limit')->inlineLabel()
|
||||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||||
->required()
|
->required()
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
])
|
||||||
->numeric()
|
->numeric()
|
||||||
->minValue(0),
|
->minValue(0),
|
||||||
TextInput::make('disk_overallocate')
|
TextInput::make('disk_overallocate')
|
||||||
@ -349,7 +417,12 @@ class EditNode extends EditRecord
|
|||||||
->label('Overallocate')->inlineLabel()
|
->label('Overallocate')->inlineLabel()
|
||||||
->hintIcon('tabler-question-mark')
|
->hintIcon('tabler-question-mark')
|
||||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
])
|
||||||
->required()
|
->required()
|
||||||
->numeric()
|
->numeric()
|
||||||
->minValue(-1)
|
->minValue(-1)
|
||||||
@ -412,8 +485,49 @@ class EditNode extends EditRecord
|
|||||||
->rows(19)
|
->rows(19)
|
||||||
->hintAction(CopyAction::make())
|
->hintAction(CopyAction::make())
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
Forms\Components\Actions::make([
|
Grid::make()
|
||||||
Forms\Components\Actions\Action::make('resetKey')
|
->columns()
|
||||||
|
->schema([
|
||||||
|
FormActions::make([
|
||||||
|
FormActions\Action::make('autoDeploy')
|
||||||
|
->label('Auto Deploy Command')
|
||||||
|
->color('primary')
|
||||||
|
->modalHeading('Auto Deploy Command')
|
||||||
|
->icon('tabler-rocket')
|
||||||
|
->modalSubmitAction(false)
|
||||||
|
->modalCancelAction(false)
|
||||||
|
->modalFooterActionsAlignment(Alignment::Center)
|
||||||
|
->form([
|
||||||
|
ToggleButtons::make('docker')
|
||||||
|
->label('Type')
|
||||||
|
->live()
|
||||||
|
->helperText('Choose between Standalone and Docker install.')
|
||||||
|
->inline()
|
||||||
|
->default(false)
|
||||||
|
->afterStateUpdated(fn (bool $state, NodeAutoDeployService $service, Node $node, Set $set) => $set('generatedToken', $service->handle(request(), $node, $state)))
|
||||||
|
->options([
|
||||||
|
false => 'Standalone',
|
||||||
|
true => 'Docker',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'primary',
|
||||||
|
true => 'success',
|
||||||
|
])
|
||||||
|
->columnSpan(1),
|
||||||
|
Textarea::make('generatedToken')
|
||||||
|
->label('To auto-configure your node run the following command:')
|
||||||
|
->readOnly()
|
||||||
|
->autosize()
|
||||||
|
->hintAction(fn (string $state) => CopyAction::make()->copyable($state))
|
||||||
|
->formatStateUsing(fn (NodeAutoDeployService $service, Node $node, Set $set, Get $get) => $set('generatedToken', $service->handle(request(), $node, $get('docker')))),
|
||||||
|
])
|
||||||
|
->mountUsing(function (Forms\Form $form) {
|
||||||
|
Notification::make()->success()->title('Autodeploy Generated')->send();
|
||||||
|
$form->fill();
|
||||||
|
}),
|
||||||
|
])->fullWidth(),
|
||||||
|
FormActions::make([
|
||||||
|
FormActions\Action::make('resetKey')
|
||||||
->label('Reset Daemon Token')
|
->label('Reset Daemon Token')
|
||||||
->color('danger')
|
->color('danger')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
@ -424,6 +538,7 @@ class EditNode extends EditRecord
|
|||||||
Notification::make()->success()->title('Daemon Key Reset')->send();
|
Notification::make()->success()->title('Daemon Key Reset')->send();
|
||||||
$this->fillForm();
|
$this->fillForm();
|
||||||
}),
|
}),
|
||||||
|
])->fullWidth(),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Models\Node;
|
use App\Models\Node;
|
||||||
use App\Models\ApiKey;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Services\Api\KeyCreationService;
|
use App\Services\Nodes\NodeAutoDeployService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
class NodeAutoDeployController extends Controller
|
class NodeAutoDeployController extends Controller
|
||||||
{
|
{
|
||||||
@ -15,48 +14,19 @@ class NodeAutoDeployController extends Controller
|
|||||||
* NodeAutoDeployController constructor.
|
* NodeAutoDeployController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private KeyCreationService $keyCreationService
|
private readonly NodeAutoDeployService $nodeAutoDeployService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new API key for the logged-in user with only permission to read
|
* Handles the API request and returns the deployment command.
|
||||||
* nodes, and returns that as the deployment key for a node.
|
|
||||||
*
|
*
|
||||||
* @throws \App\Exceptions\Model\DataValidationException
|
* @throws \App\Exceptions\Model\DataValidationException
|
||||||
*/
|
*/
|
||||||
public function __invoke(Request $request, Node $node): JsonResponse
|
public function __invoke(Request $request, Node $node): JsonResponse
|
||||||
{
|
{
|
||||||
$keys = $request->user()->apiKeys()
|
$command = $this->nodeAutoDeployService->handle($request, $node);
|
||||||
->where('key_type', ApiKey::TYPE_APPLICATION)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
/** @var ApiKey|null $key */
|
return new JsonResponse(['command' => $command]);
|
||||||
$key = $keys
|
|
||||||
->filter(function (ApiKey $key) {
|
|
||||||
foreach ($key->getAttributes() as $permission => $value) {
|
|
||||||
if ($permission === 'r_nodes' && $value === 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
->first();
|
|
||||||
|
|
||||||
// We couldn't find a key that exists for this user with only permission for
|
|
||||||
// reading nodes. Go ahead and create it now.
|
|
||||||
if (!$key) {
|
|
||||||
$key = $this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([
|
|
||||||
'user_id' => $request->user()->id,
|
|
||||||
'memo' => 'Automatically generated node deployment key.',
|
|
||||||
'allowed_ips' => [],
|
|
||||||
], ['r_nodes' => 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonResponse([
|
|
||||||
'node' => $node->id,
|
|
||||||
'token' => $key->identifier . $key->token,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ class ApiKeyController extends ClientApiController
|
|||||||
*/
|
*/
|
||||||
public function store(StoreApiKeyRequest $request): array
|
public function store(StoreApiKeyRequest $request): array
|
||||||
{
|
{
|
||||||
if ($request->user()->apiKeys->count() >= 25) {
|
if ($request->user()->apiKeys->count() >= config('panel.api.key_limit')) {
|
||||||
throw new DisplayException('You have reached the account limit for number of API keys.');
|
throw new DisplayException('You have reached the account limit for number of API keys.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,15 +71,8 @@ class ApiKey extends Model
|
|||||||
|
|
||||||
public const TYPE_ACCOUNT = 1;
|
public const TYPE_ACCOUNT = 1;
|
||||||
|
|
||||||
/* @deprecated */
|
|
||||||
public const TYPE_APPLICATION = 2;
|
public const TYPE_APPLICATION = 2;
|
||||||
|
|
||||||
/* @deprecated */
|
|
||||||
public const TYPE_DAEMON_USER = 3;
|
|
||||||
|
|
||||||
/* @deprecated */
|
|
||||||
public const TYPE_DAEMON_APPLICATION = 4;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The length of API key identifiers.
|
* The length of API key identifiers.
|
||||||
*/
|
*/
|
||||||
@ -138,7 +131,7 @@ class ApiKey extends Model
|
|||||||
*/
|
*/
|
||||||
public static array $validationRules = [
|
public static array $validationRules = [
|
||||||
'user_id' => 'required|exists:users,id',
|
'user_id' => 'required|exists:users,id',
|
||||||
'key_type' => 'present|integer|min:0|max:4',
|
'key_type' => 'present|integer|min:0|max:2',
|
||||||
'identifier' => 'required|string|size:16|unique:api_keys,identifier',
|
'identifier' => 'required|string|size:16|unique:api_keys,identifier',
|
||||||
'token' => 'required|string',
|
'token' => 'required|string',
|
||||||
'memo' => 'required|nullable|string|max:500',
|
'memo' => 'required|nullable|string|max:500',
|
||||||
|
58
app/Services/Nodes/NodeAutoDeployService.php
Normal file
58
app/Services/Nodes/NodeAutoDeployService.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Nodes;
|
||||||
|
|
||||||
|
use App\Models\ApiKey;
|
||||||
|
use App\Models\Node;
|
||||||
|
use App\Services\Api\KeyCreationService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class NodeAutoDeployService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* NodeAutoDeployService constructor.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly KeyCreationService $keyCreationService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new API key for the logged-in user with only permission to read
|
||||||
|
* nodes, and returns that as the deployment key for a node.
|
||||||
|
*
|
||||||
|
* @throws \App\Exceptions\Model\DataValidationException
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Node $node, ?bool $docker = false): ?string
|
||||||
|
{
|
||||||
|
/** @var ApiKey|null $key */
|
||||||
|
$key = ApiKey::query()
|
||||||
|
->where('key_type', ApiKey::TYPE_APPLICATION)
|
||||||
|
->where('r_nodes', true)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// We couldn't find a key that exists for this user with only permission for
|
||||||
|
// reading nodes. Go ahead and create it now.
|
||||||
|
if (!$key) {
|
||||||
|
$key = $this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([
|
||||||
|
'memo' => 'Automatically generated node deployment key.',
|
||||||
|
'user_id' => $request->user()->id,
|
||||||
|
], ['r_nodes' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $key->identifier . $key->token;
|
||||||
|
|
||||||
|
if (!$token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%s wings configure --panel-url %s --token %s --node %d%s',
|
||||||
|
$docker ? 'docker compose exec -it' : 'sudo',
|
||||||
|
config('app.url'),
|
||||||
|
$token,
|
||||||
|
$node->id,
|
||||||
|
$request->isSecure() ? '' : ' --allow-insecure'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -167,4 +167,9 @@ return [
|
|||||||
'use_binary_prefix' => env('PANEL_USE_BINARY_PREFIX', true),
|
'use_binary_prefix' => env('PANEL_USE_BINARY_PREFIX', true),
|
||||||
|
|
||||||
'editable_server_descriptions' => env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', true),
|
'editable_server_descriptions' => env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', true),
|
||||||
|
|
||||||
|
'api' => [
|
||||||
|
'key_limit' => env('API_KEYS_LIMIT', 25),
|
||||||
|
'key_expire_time' => env('API_KEYS_EXPIRE_TIME', 720),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="bootstrap/tests.php" colors="true">
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||||
|
bootstrap="bootstrap/tests.php"
|
||||||
|
colors="true"
|
||||||
|
displayDetailsOnSkippedTests="true"
|
||||||
|
>
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="Integration">
|
<testsuite name="Integration">
|
||||||
<directory>./tests/Integration</directory>
|
<directory>./tests/Integration</directory>
|
||||||
|
@ -96,14 +96,14 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that no more than 25 API keys can exist at any one time for an account. This prevents
|
* Test that no more than the Max number of API keys can exist at one time for an account. This prevents
|
||||||
* a DoS attack vector against the panel.
|
* a DoS attack vector against the panel.
|
||||||
*/
|
*/
|
||||||
public function testApiKeyLimitIsApplied(): void
|
public function testApiKeyLimitIsApplied(): void
|
||||||
{
|
{
|
||||||
/** @var \App\Models\User $user */
|
/** @var \App\Models\User $user */
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
ApiKey::factory()->times(25)->for($user)->create([
|
ApiKey::factory()->times(config('panel.api.key_limit', 25))->for($user)->create([
|
||||||
'key_type' => ApiKey::TYPE_ACCOUNT,
|
'key_type' => ApiKey::TYPE_ACCOUNT,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user