mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-02 14:36:17 +00:00
Add load balancer (#453)
This commit is contained in:
@ -46,7 +46,6 @@ public function create(Site $site, array $input): void
|
||||
$ssl->status = SslStatus::CREATED;
|
||||
$ssl->save();
|
||||
$webserver->updateVHost($site);
|
||||
$site->type()->edit();
|
||||
})->catch(function () use ($ssl) {
|
||||
$ssl->status = SslStatus::FAILED;
|
||||
$ssl->save();
|
||||
|
@ -24,6 +24,9 @@ public function edit(Server $server, array $input): Server
|
||||
}
|
||||
$server->ip = $input['ip'];
|
||||
}
|
||||
if (isset($input['local_ip'])) {
|
||||
$server->local_ip = $input['local_ip'];
|
||||
}
|
||||
if (isset($input['port'])) {
|
||||
if ($server->port !== $input['port']) {
|
||||
$checkConnection = true;
|
||||
@ -52,6 +55,10 @@ public static function rules(Server $server): array
|
||||
new RestrictedIPAddressesRule,
|
||||
Rule::unique('servers')->where('project_id', $server->project_id)->ignore($server->id),
|
||||
],
|
||||
'local_ip' => [
|
||||
'string',
|
||||
Rule::unique('servers')->where('project_id', $server->project_id)->ignore($server->id),
|
||||
],
|
||||
'port' => [
|
||||
'integer',
|
||||
'min:1',
|
||||
|
63
app/Actions/Site/UpdateLoadBalancer.php
Normal file
63
app/Actions/Site/UpdateLoadBalancer.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Site;
|
||||
|
||||
use App\Enums\LoadBalancerMethod;
|
||||
use App\Models\LoadBalancerServer;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateLoadBalancer
|
||||
{
|
||||
public function update(Site $site, array $input): void
|
||||
{
|
||||
$site->loadBalancerServers()->delete();
|
||||
|
||||
foreach ($input['servers'] as $server) {
|
||||
$loadBalancerServer = new LoadBalancerServer([
|
||||
'load_balancer_id' => $site->id,
|
||||
'ip' => $server['server'],
|
||||
'port' => $server['port'],
|
||||
'weight' => $server['weight'],
|
||||
'backup' => (bool) $server['backup'],
|
||||
]);
|
||||
$loadBalancerServer->save();
|
||||
}
|
||||
|
||||
$site->webserver()->updateVHost($site);
|
||||
}
|
||||
|
||||
public static function rules(Site $site): array
|
||||
{
|
||||
return [
|
||||
'servers' => [
|
||||
'required',
|
||||
'array',
|
||||
],
|
||||
'servers.*.server' => [
|
||||
'required',
|
||||
Rule::exists('servers', 'local_ip')
|
||||
->where('project_id', $site->project->id),
|
||||
],
|
||||
'servers.*.port' => [
|
||||
'required',
|
||||
'numeric',
|
||||
'min:1',
|
||||
'max:65535',
|
||||
],
|
||||
'servers.*.weight' => [
|
||||
'nullable',
|
||||
'numeric',
|
||||
'min:0',
|
||||
],
|
||||
'servers.*.backup' => [
|
||||
'required',
|
||||
'boolean',
|
||||
],
|
||||
'method' => [
|
||||
'required',
|
||||
Rule::in(LoadBalancerMethod::all()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
16
app/Enums/LoadBalancerMethod.php
Normal file
16
app/Enums/LoadBalancerMethod.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\Enum;
|
||||
|
||||
final class LoadBalancerMethod
|
||||
{
|
||||
use Enum;
|
||||
|
||||
const ROUND_ROBIN = 'round-robin';
|
||||
|
||||
const LEAST_CONNECTIONS = 'least-connections';
|
||||
|
||||
const IP_HASH = 'ip-hash';
|
||||
}
|
@ -14,20 +14,5 @@ final class SiteType
|
||||
|
||||
const PHPMYADMIN = 'phpmyadmin';
|
||||
|
||||
public static function hasWebDirectory(): array
|
||||
{
|
||||
return [
|
||||
self::PHP,
|
||||
self::PHP_BLANK,
|
||||
self::LARAVEL,
|
||||
];
|
||||
}
|
||||
|
||||
public static function hasSourceControl(): array
|
||||
{
|
||||
return [
|
||||
self::PHP,
|
||||
self::LARAVEL,
|
||||
];
|
||||
}
|
||||
const LOAD_BALANCER = 'load-balancer';
|
||||
}
|
||||
|
@ -3,9 +3,10 @@
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Actions\Site\CreateSite;
|
||||
use App\Actions\Site\UpdateLoadBalancer;
|
||||
use App\Enums\LoadBalancerMethod;
|
||||
use App\Enums\SiteType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ServerResource;
|
||||
use App\Http\Resources\SiteResource;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
@ -42,7 +43,7 @@ public function index(Project $project, Server $server): ResourceCollection
|
||||
|
||||
#[Post('/', name: 'api.projects.servers.sites.create', middleware: 'ability:write')]
|
||||
#[Endpoint(title: 'create', description: 'Create a new site.')]
|
||||
#[BodyParam(name: 'type', required: true, enum: [SiteType::PHP, SiteType::PHP_BLANK, SiteType::PHPMYADMIN, SiteType::LARAVEL, SiteType::WORDPRESS])]
|
||||
#[BodyParam(name: 'type', required: true, enum: [SiteType::PHP, SiteType::PHP_BLANK, SiteType::PHPMYADMIN, SiteType::LARAVEL, SiteType::WORDPRESS, SiteType::LOAD_BALANCER])]
|
||||
#[BodyParam(name: 'domain', required: true)]
|
||||
#[BodyParam(name: 'aliases', type: 'array')]
|
||||
#[BodyParam(name: 'php_version', description: 'One of the installed PHP Versions', required: true, example: '7.4')]
|
||||
@ -53,6 +54,7 @@ public function index(Project $project, Server $server): ResourceCollection
|
||||
#[BodyParam(name: 'composer', type: 'boolean', description: 'Run composer if site supports composer', example: true)]
|
||||
#[BodyParam(name: 'version', description: 'Version, if the site type requires a version like PHPMyAdmin', example: '5.2.1')]
|
||||
#[BodyParam(name: 'user', description: 'user, to isolate the website under a new user')]
|
||||
#[BodyParam(name: 'method', description: 'Load balancer method, Required if the site type is Load balancer', enum: [LoadBalancerMethod::ROUND_ROBIN, LoadBalancerMethod::LEAST_CONNECTIONS, LoadBalancerMethod::IP_HASH])]
|
||||
#[ResponseFromApiResource(SiteResource::class, Site::class)]
|
||||
public function create(Request $request, Project $project, Server $server): SiteResource
|
||||
{
|
||||
@ -70,13 +72,13 @@ public function create(Request $request, Project $project, Server $server): Site
|
||||
#[Get('{site}', name: 'api.projects.servers.sites.show', middleware: 'ability:read')]
|
||||
#[Endpoint(title: 'show', description: 'Get a site by ID.')]
|
||||
#[ResponseFromApiResource(SiteResource::class, Site::class)]
|
||||
public function show(Project $project, Server $server, Site $site): ServerResource
|
||||
public function show(Project $project, Server $server, Site $site): SiteResource
|
||||
{
|
||||
$this->authorize('view', [$site, $server]);
|
||||
|
||||
$this->validateRoute($project, $server, $site);
|
||||
|
||||
return new ServerResource($server);
|
||||
return new SiteResource($site);
|
||||
}
|
||||
|
||||
#[Delete('{site}', name: 'api.projects.servers.sites.delete', middleware: 'ability:write')]
|
||||
@ -93,6 +95,24 @@ public function delete(Project $project, Server $server, Site $site)
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
#[Post('{site}/load-balancer', name: 'api.projects.servers.sites.load-balancer', middleware: 'ability:write')]
|
||||
#[Endpoint(title: 'load-balancer', description: 'Update load balancer.')]
|
||||
#[BodyParam(name: 'method', description: 'Load balancer method, Required if the site type is Load balancer', enum: [LoadBalancerMethod::ROUND_ROBIN, LoadBalancerMethod::LEAST_CONNECTIONS, LoadBalancerMethod::IP_HASH])]
|
||||
#[BodyParam(name: 'servers', type: 'array', description: 'Array of servers including server, port, weight, backup. (server is the local IP of the server)')]
|
||||
#[Response(status: 200)]
|
||||
public function updateLoadBalancer(Request $request, Project $project, Server $server, Site $site): SiteResource
|
||||
{
|
||||
$this->authorize('update', [$site, $server]);
|
||||
|
||||
$this->validateRoute($project, $server, $site);
|
||||
|
||||
$this->validate($request, UpdateLoadBalancer::rules($site));
|
||||
|
||||
app(UpdateLoadBalancer::class)->update($site, $request->all());
|
||||
|
||||
return new SiteResource($site);
|
||||
}
|
||||
|
||||
private function validateRoute(Project $project, Server $server, ?Site $site = null): void
|
||||
{
|
||||
if ($project->id !== $server->project_id) {
|
||||
|
44
app/Models/LoadBalancerServer.php
Normal file
44
app/Models/LoadBalancerServer.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $load_balancer_id
|
||||
* @property string $ip
|
||||
* @property int $port
|
||||
* @property int $weight
|
||||
* @property bool $backup
|
||||
* @property Site $loadBalancer
|
||||
*/
|
||||
class LoadBalancerServer extends AbstractModel
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'load_balancer_id',
|
||||
'ip',
|
||||
'port',
|
||||
'weight',
|
||||
'backup',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'load_balancer_id' => 'integer',
|
||||
'port' => 'integer',
|
||||
'weight' => 'integer',
|
||||
'backup' => 'boolean',
|
||||
];
|
||||
|
||||
public function loadBalancer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class, 'load_balancer_id');
|
||||
}
|
||||
|
||||
public function server(): ?Server
|
||||
{
|
||||
return $this->loadBalancer->project->servers()->where('local_ip', $this->ip)->first();
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@ -47,6 +48,7 @@
|
||||
* @property ?Ssl $activeSsl
|
||||
* @property string $ssh_key_name
|
||||
* @property ?SourceControl $sourceControl
|
||||
* @property Collection<LoadBalancerServer> $loadBalancerServers
|
||||
*
|
||||
* @TODO: Add nodejs_version column
|
||||
*/
|
||||
@ -332,4 +334,9 @@ public function webserver(): Webserver
|
||||
|
||||
return $webserver;
|
||||
}
|
||||
|
||||
public function loadBalancerServers(): HasMany
|
||||
{
|
||||
return $this->hasMany(LoadBalancerServer::class, 'load_balancer_id');
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ public function data(array $input): array
|
||||
|
||||
public function editRules(array $input): array
|
||||
{
|
||||
return [];
|
||||
return $this->createRules($input);
|
||||
}
|
||||
|
||||
protected function progress(int $percentage): void
|
||||
|
58
app/SiteTypes/LoadBalancer.php
Executable file
58
app/SiteTypes/LoadBalancer.php
Executable file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\SiteTypes;
|
||||
|
||||
use App\Enums\LoadBalancerMethod;
|
||||
use App\Enums\SiteFeature;
|
||||
use App\Exceptions\SSHError;
|
||||
use App\SSH\Services\Webserver\Webserver;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class LoadBalancer extends AbstractSiteType
|
||||
{
|
||||
public function language(): string
|
||||
{
|
||||
return 'yaml';
|
||||
}
|
||||
|
||||
public function supportedFeatures(): array
|
||||
{
|
||||
return [
|
||||
SiteFeature::SSL,
|
||||
];
|
||||
}
|
||||
|
||||
public function createRules(array $input): array
|
||||
{
|
||||
return [
|
||||
'method' => [
|
||||
'required',
|
||||
Rule::in(LoadBalancerMethod::all()),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function data(array $input): array
|
||||
{
|
||||
return [
|
||||
'method' => $input['method'] ?? LoadBalancerMethod::ROUND_ROBIN,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHError
|
||||
*/
|
||||
public function install(): void
|
||||
{
|
||||
$this->isolate();
|
||||
|
||||
/** @var Webserver $webserver */
|
||||
$webserver = $this->site->server->webserver()->handler();
|
||||
$webserver->createVHost($this->site);
|
||||
}
|
||||
|
||||
public function edit(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace App\Web\Pages\Servers\Sites;
|
||||
|
||||
use App\Actions\Site\CreateSite;
|
||||
use App\Enums\LoadBalancerMethod;
|
||||
use App\Enums\SiteType;
|
||||
use App\Models\Site;
|
||||
use App\Models\SourceControl;
|
||||
@ -133,6 +134,17 @@ protected function getHeaderActions(): array
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['version']),
|
||||
// WordPress
|
||||
$this->wordpressFields(),
|
||||
// Load Balancer
|
||||
Select::make('method')
|
||||
->label('Balancing Method')
|
||||
->validationAttribute('Balancing Method')
|
||||
->options(
|
||||
collect(LoadBalancerMethod::all())
|
||||
->mapWithKeys(fn ($method) => [$method => $method])
|
||||
)
|
||||
->visible(fn (Get $get) => $get('type') === SiteType::LOAD_BALANCER)
|
||||
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['method'] ?? []),
|
||||
// User
|
||||
TextInput::make('user')
|
||||
->label('User')
|
||||
->placeholder('vito')
|
||||
|
@ -7,6 +7,7 @@
|
||||
use App\Actions\Site\UpdateDeploymentScript;
|
||||
use App\Actions\Site\UpdateEnv;
|
||||
use App\Enums\SiteFeature;
|
||||
use App\Enums\SiteType;
|
||||
use App\Web\Fields\CodeEditorField;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
@ -58,6 +59,10 @@ public function getWidgets(): array
|
||||
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
|
||||
$widgets[] = [Widgets\DeploymentsList::class, ['site' => $this->site]];
|
||||
}
|
||||
|
||||
if ($this->site->type === SiteType::LOAD_BALANCER) {
|
||||
$widgets[] = [Widgets\LoadBalancerServers::class, ['site' => $this->site]];
|
||||
}
|
||||
}
|
||||
|
||||
return $widgets;
|
||||
|
141
app/Web/Pages/Servers/Sites/Widgets/LoadBalancerServers.php
Normal file
141
app/Web/Pages/Servers/Sites/Widgets/LoadBalancerServers.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Sites\Widgets;
|
||||
|
||||
use App\Actions\Site\UpdateLoadBalancer;
|
||||
use App\Enums\LoadBalancerMethod;
|
||||
use App\Models\LoadBalancerServer;
|
||||
use App\Models\Site;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\Widget;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class LoadBalancerServers extends Widget implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'components.form';
|
||||
|
||||
public Site $site;
|
||||
|
||||
public string $method;
|
||||
|
||||
public array $servers = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->setLoadBalancerServers();
|
||||
if (empty($this->servers)) {
|
||||
$this->servers = [
|
||||
[
|
||||
'server' => null,
|
||||
'port' => 80,
|
||||
'weight' => null,
|
||||
'backup' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
$this->method = $this->site->type_data['method'] ?? LoadBalancerMethod::ROUND_ROBIN;
|
||||
}
|
||||
|
||||
#[On('load-balancer-updated')]
|
||||
public function setLoadBalancerServers(): void
|
||||
{
|
||||
$this->servers = $this->site->loadBalancerServers->map(function (LoadBalancerServer $server) {
|
||||
return [
|
||||
'server' => $server->ip,
|
||||
'port' => $server->port,
|
||||
'weight' => $server->weight,
|
||||
'backup' => $server->backup,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->heading('Load Balancer Servers')
|
||||
->description('You can add or remove servers from the load balancer here')
|
||||
->columns(3)
|
||||
->schema([
|
||||
Select::make('method')
|
||||
->label('Balancing Method')
|
||||
->validationAttribute('Balancing Method')
|
||||
->options(
|
||||
collect(LoadBalancerMethod::all())
|
||||
->mapWithKeys(fn ($method) => [$method => $method])
|
||||
)
|
||||
->rules(UpdateLoadBalancer::rules($this->site)['method']),
|
||||
Repeater::make('servers')
|
||||
->schema([
|
||||
Select::make('server')
|
||||
->placeholder('Select a server')
|
||||
->searchable()
|
||||
->required()
|
||||
->rules(UpdateLoadBalancer::rules($this->site)['servers.*.server'])
|
||||
->options(function () {
|
||||
return $this->site->project->servers()
|
||||
->where('id', '!=', $this->site->server_id)
|
||||
->get()
|
||||
->mapWithKeys(function ($server) {
|
||||
return [$server->local_ip => $server->name.' ('.$server->local_ip.')'];
|
||||
});
|
||||
}),
|
||||
TextInput::make('port')
|
||||
->default(80)
|
||||
->required()
|
||||
->rules(UpdateLoadBalancer::rules($this->site)['servers.*.port']),
|
||||
TextInput::make('weight')
|
||||
->rules(UpdateLoadBalancer::rules($this->site)['servers.*.weight']),
|
||||
Toggle::make('backup')
|
||||
->label('Backup')
|
||||
->inline(false)
|
||||
->default(false),
|
||||
])
|
||||
->columnSpan(3)
|
||||
->live()
|
||||
->reorderable(false)
|
||||
->columns(4)
|
||||
->reorderableWithDragAndDrop(false)
|
||||
->addActionLabel('Add Server'),
|
||||
])
|
||||
->footerActions([
|
||||
Action::make('save')
|
||||
->label('Save')
|
||||
->action(fn () => $this->save()),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('update', [$this->site, $this->site->server]);
|
||||
|
||||
$this->validate();
|
||||
|
||||
run_action($this, function () {
|
||||
app(UpdateLoadBalancer::class)->update($this->site, [
|
||||
'method' => $this->method,
|
||||
'servers' => $this->servers,
|
||||
]);
|
||||
|
||||
$this->dispatch('load-balancer-updated');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Load balancer updated!')
|
||||
->send();
|
||||
});
|
||||
}
|
||||
}
|
@ -27,6 +27,8 @@ class UpdateServerInfo extends Widget implements HasForms
|
||||
|
||||
public string $ip;
|
||||
|
||||
public ?string $local_ip;
|
||||
|
||||
public string $port;
|
||||
|
||||
public function mount(Server $server): void
|
||||
@ -34,6 +36,7 @@ public function mount(Server $server): void
|
||||
$this->server = $server;
|
||||
$this->name = $server->name;
|
||||
$this->ip = $server->ip;
|
||||
$this->local_ip = $server->local_ip;
|
||||
$this->port = $server->port;
|
||||
}
|
||||
|
||||
@ -52,6 +55,10 @@ public function form(Form $form): Form
|
||||
TextInput::make('ip')
|
||||
->label('IP Address')
|
||||
->rules(EditServer::rules($this->server)['ip']),
|
||||
TextInput::make('local_ip')
|
||||
->label('Local Network IP Address')
|
||||
->placeholder('10.0.0.1')
|
||||
->rules(EditServer::rules($this->server)['local_ip']),
|
||||
TextInput::make('port')
|
||||
->label('Port')
|
||||
->rules(EditServer::rules($this->server)['port']),
|
||||
|
Reference in New Issue
Block a user