This commit is contained in:
2025-04-07 12:16:11 +01:00
parent ce8b201a1c
commit e15a80163b
62 changed files with 149 additions and 131 deletions

View File

@@ -20,6 +20,7 @@ class InstallFirewallRule
$firewallRule->update([ $firewallRule->update([
'status' => FirewallRuleStatus::FAILED, 'status' => FirewallRuleStatus::FAILED,
]); ]);
return; return;
} }

View File

@@ -20,6 +20,7 @@ class UninstallFirewallRule
$firewallRule->update([ $firewallRule->update([
'status' => FirewallRuleStatus::FAILED, 'status' => FirewallRuleStatus::FAILED,
]); ]);
return; return;
} }

View File

@@ -12,7 +12,7 @@ class GenerateRandomSlug
$slug = ''; $slug = '';
for ($i = 0; $i < $adjectiveCount; $i++) { for ($i = 0; $i < $adjectiveCount; $i++) {
$slug .= $adjectives[array_rand($adjectives)] . '-'; $slug .= $adjectives[array_rand($adjectives)].'-';
} }
$slug .= $nouns[array_rand($nouns)]; $slug .= $nouns[array_rand($nouns)];

View File

@@ -7,10 +7,10 @@ use App\Services\ServerProviders\ServerProviderService;
class GetProviderService class GetProviderService
{ {
public function execute(string $provider): ServerProviderService|null public function execute(string $provider): ?ServerProviderService
{ {
return match ($provider) { return match ($provider) {
'hetzner' => new HetznerService(), 'hetzner' => new HetznerService,
default => null, default => null,
}; };
} }

View File

@@ -11,6 +11,7 @@ use Illuminate\Console\Command;
class CreateServiceCommand extends Command class CreateServiceCommand extends Command
{ {
protected $signature = 'service:create'; protected $signature = 'service:create';
protected $description = 'Create a service'; protected $description = 'Create a service';
public function handle() public function handle()
@@ -18,17 +19,18 @@ class CreateServiceCommand extends Command
$serverId = $this->components->ask('Enter the server ID'); $serverId = $this->components->ask('Enter the server ID');
$server = Server::find($serverId); $server = Server::find($serverId);
if (!$server) { if (! $server) {
$this->components->error('Server not found'); $this->components->error('Server not found');
return; return;
} }
$serviceType = $this->components->choice('select the service you want to install', [ $serviceType = $this->components->choice('select the service you want to install', [
'postgres-17' 'postgres-17',
]); ]);
$serviceName = $this->components->ask('Enter the service name'); $serviceName = $this->components->ask('Enter the service name');
list ($type, $version) = explode('-', $serviceType); [$type, $version] = explode('-', $serviceType);
$service = app(CreateService::class)->execute( $service = app(CreateService::class)->execute(
server: $server, server: $server,

View File

@@ -40,10 +40,10 @@ class GenerateJSEnums extends Command
} }
foreach ((new Finder)->in($paths)->files() as $enum) { foreach ((new Finder)->in($paths)->files() as $enum) {
$enum = 'App\\' . str_replace( $enum = 'App\\'.str_replace(
['/', '.php'], ['/', '.php'],
['\\', ''], ['\\', ''],
Str::after($enum->getRealPath(), realpath(app_path()) . DIRECTORY_SEPARATOR) Str::after($enum->getRealPath(), realpath(app_path()).DIRECTORY_SEPARATOR)
); );
if (! class_exists($enum)) { if (! class_exists($enum)) {
@@ -51,36 +51,36 @@ class GenerateJSEnums extends Command
} }
$js = "// This is a generated file. \n"; $js = "// This is a generated file. \n";
$js .= '// Published at ' . now()->format('Y-m-d H:i:s') . "\n"; $js .= '// Published at '.now()->format('Y-m-d H:i:s')."\n";
$js .= "\n"; $js .= "\n";
$js .= 'export default '; $js .= 'export default ';
$js .= json_encode($enum::toArray(), JSON_PRETTY_PRINT) . "\n"; $js .= json_encode($enum::toArray(), JSON_PRETTY_PRINT)."\n";
$js .= "\n"; $js .= "\n";
if (method_exists($enum, 'getLabels')) { if (method_exists($enum, 'getLabels')) {
$labels = $enum::getLabels(); $labels = $enum::getLabels();
$js .= 'export const LabelMap = '; $js .= 'export const LabelMap = ';
$js .= json_encode($labels, JSON_PRETTY_PRINT) . "\n"; $js .= json_encode($labels, JSON_PRETTY_PRINT)."\n";
$js .= "\n"; $js .= "\n";
$labelSelect = array_map(fn($key) => ['title' => $labels[$key], 'id' => $key], array_keys($labels)); $labelSelect = array_map(fn ($key) => ['title' => $labels[$key], 'id' => $key], array_keys($labels));
$js .= 'export const LabelSelectMap = '; $js .= 'export const LabelSelectMap = ';
$js .= json_encode($labelSelect, JSON_PRETTY_PRINT) . "\n"; $js .= json_encode($labelSelect, JSON_PRETTY_PRINT)."\n";
$js .= "\n"; $js .= "\n";
} }
if (method_exists($enum, 'getDescription')) { if (method_exists($enum, 'getDescription')) {
$values = $enum::toArray(); $values = $enum::toArray();
$descriptions = array_map(fn($key) => $enum::getDescription($key), $values); $descriptions = array_map(fn ($key) => $enum::getDescription($key), $values);
$js .= 'export const DescriptionMap = '; $js .= 'export const DescriptionMap = ';
$js .= json_encode($descriptions, JSON_PRETTY_PRINT) . "\n"; $js .= json_encode($descriptions, JSON_PRETTY_PRINT)."\n";
$js .= "\n"; $js .= "\n";
} }
if (method_exists($enum, 'colours')) { if (method_exists($enum, 'colours')) {
$colours = $enum::colours(); $colours = $enum::colours();
$js .= 'export const ColourMap = '; $js .= 'export const ColourMap = ';
$js .= json_encode($colours, JSON_PRETTY_PRINT) . "\n"; $js .= json_encode($colours, JSON_PRETTY_PRINT)."\n";
$js .= "\n"; $js .= "\n";
} }
@@ -88,10 +88,10 @@ class GenerateJSEnums extends Command
// Skip format, JS date formats are different to PHP ones. // Skip format, JS date formats are different to PHP ones.
if ($name !== 'Format') { if ($name !== 'Format') {
file_put_contents(base_path('resources/js/Enums/' . $name . '.js'), $js); file_put_contents(base_path('resources/js/Enums/'.$name.'.js'), $js);
$this->info('Stored ' . $enum); $this->info('Stored '.$enum);
} else { } else {
$this->info('Skipped ' . $name . 's'); $this->info('Skipped '.$name.'s');
} }
} }
} }

View File

@@ -8,27 +8,30 @@ use Illuminate\Support\Facades\Process;
class GenerateSshKey extends Command class GenerateSshKey extends Command
{ {
protected $signature = 'setup:generate-ssh-key'; protected $signature = 'setup:generate-ssh-key';
protected $description = 'Generates an SSH key pair for the application.'; protected $description = 'Generates an SSH key pair for the application.';
public function handle() public function handle()
{ {
if (file_exists(storage_path('app/private/ssh/id_ed25519'))) { if (file_exists(storage_path('app/private/ssh/id_ed25519'))) {
$this->components->info('SSH key pair already exists.'); $this->components->info('SSH key pair already exists.');
return; return;
} }
$this->components->info('Generating SSH key pair...'); $this->components->info('Generating SSH key pair...');
if (!file_exists(storage_path('app/private/ssh'))) { if (! file_exists(storage_path('app/private/ssh'))) {
$this->components->info('ssh directory does not exist. Creating it now...'); $this->components->info('ssh directory does not exist. Creating it now...');
mkdir(storage_path('app/private/ssh'), 0755, true); mkdir(storage_path('app/private/ssh'), 0755, true);
} }
$result = Process::run(['ssh-keygen', '-t', 'ed25519', '-f', storage_path('app/private/ssh/id_ed25519'), '-N', '']); $result = Process::run(['ssh-keygen', '-t', 'ed25519', '-f', storage_path('app/private/ssh/id_ed25519'), '-N', '']);
if (!$result->successful()) { if (! $result->successful()) {
$this->components->error('Failed to generate SSH key pair.'); $this->components->error('Failed to generate SSH key pair.');
$this->line($result->output()); $this->line($result->output());
$this->line($result->errorOutput()); $this->line($result->errorOutput());
return; return;
} }

View File

@@ -7,6 +7,7 @@ use Illuminate\Console\Command;
class Setup extends Command class Setup extends Command
{ {
protected $signature = 'setup'; protected $signature = 'setup';
protected $description = 'Initialize the application.'; protected $description = 'Initialize the application.';
public function handle() public function handle()

View File

@@ -5,7 +5,7 @@ namespace App\Data\Deployments;
class Plan class Plan
{ {
/** /**
* @param PlannedStep[] $steps * @param PlannedStep[] $steps
*/ */
public function __construct( public function __construct(
public array $steps = [], public array $steps = [],

View File

@@ -24,6 +24,7 @@ class PlannedStep
foreach ($this->secrets as $key => $value) { foreach ($this->secrets as $key => $value) {
$script = str_replace("[!{$key}]", '********', $script); $script = str_replace("[!{$key}]", '********', $script);
} }
return $script; return $script;
} }
@@ -33,6 +34,7 @@ class PlannedStep
foreach ($this->secrets as $key => $value) { foreach ($this->secrets as $key => $value) {
$script = str_replace("[!{$key}]", $value, $script); $script = str_replace("[!{$key}]", $value, $script);
} }
return $script; return $script;
} }
} }

View File

@@ -5,10 +5,10 @@ namespace App\Data\ServerProviders;
class ServerType class ServerType
{ {
/** /**
* @param string $name The name of the server type * @param string $name The name of the server type
* @param int $cores The number of cores * @param int $cores The number of cores
* @param int $memory The amount of memory in GB * @param int $memory The amount of memory in GB
* @param int $disk The amount of disk space in GB * @param int $disk The amount of disk space in GB
*/ */
public function __construct( public function __construct(
public string $id, public string $id,

View File

@@ -5,9 +5,13 @@ namespace App\Drivers;
abstract class DatabaseDriver extends Driver abstract class DatabaseDriver extends Driver
{ {
public string $defaultUser = 'keystone'; public string $defaultUser = 'keystone';
public string $defaultDb = 'keystone'; public string $defaultDb = 'keystone';
public ?string $containerName; public ?string $containerName;
public ?string $containerId; public ?string $containerId;
public ?string $defaultPassword; public ?string $defaultPassword;
abstract public function __construct( abstract public function __construct(

View File

@@ -7,7 +7,9 @@ use App\Data\Deployments\Plan;
abstract class Driver abstract class Driver
{ {
public Plan $deploymentPlan; public Plan $deploymentPlan;
public ?string $containerName; public ?string $containerName;
public ?string $containerId; public ?string $containerId;
abstract public function __construct( abstract public function __construct(

View File

@@ -9,15 +9,16 @@ use App\Drivers\DatabaseDriver;
class Postgres17Driver extends DatabaseDriver class Postgres17Driver extends DatabaseDriver
{ {
public Plan $deploymentPlan; public Plan $deploymentPlan;
public string $defaultUser = 'keystone'; public string $defaultUser = 'keystone';
public string $defaultDb = 'keystone'; public string $defaultDb = 'keystone';
public function __construct( public function __construct(
public ?string $containerName = null, public ?string $containerName = null,
public ?string $containerId = null, public ?string $containerId = null,
public ?string $defaultPassword = null, public ?string $defaultPassword = null,
) ) {
{
$this->deploymentPlan = new Plan(steps: [ $this->deploymentPlan = new Plan(steps: [
new Step( new Step(
name: 'Run the docker image', name: 'Run the docker image',
@@ -27,17 +28,17 @@ class Postgres17Driver extends DatabaseDriver
script: function () { script: function () {
$script = collect(); $script = collect();
if ($this->containerName) { if ($this->containerName) {
$script->push('docker stop ' . $this->containerName . ' || true'); $script->push('docker stop '.$this->containerName.' || true');
} else if ($this->containerId) { } elseif ($this->containerId) {
$script->push('docker stop ' . $this->containerId . ' || true'); $script->push('docker stop '.$this->containerId.' || true');
} }
$runCommand = "docker run -d"; $runCommand = 'docker run -d';
if ($this->containerName) { if ($this->containerName) {
$runCommand .= " --name {$this->containerName}"; $runCommand .= " --name {$this->containerName}";
} }
if ($this->defaultPassword) { if ($this->defaultPassword) {
$runCommand .= " -e POSTGRES_PASSWORD=[!defaultPassword!]"; $runCommand .= ' -e POSTGRES_PASSWORD=[!defaultPassword!]';
} }
if ($this->defaultUser) { if ($this->defaultUser) {
$runCommand .= " -e POSTGRES_USER={$this->defaultUser}"; $runCommand .= " -e POSTGRES_USER={$this->defaultUser}";
@@ -46,7 +47,7 @@ class Postgres17Driver extends DatabaseDriver
$runCommand .= " -e POSTGRES_DB={$this->defaultDb}"; $runCommand .= " -e POSTGRES_DB={$this->defaultDb}";
} }
$runCommand .= " -p 5432:5432 postgres:17"; $runCommand .= ' -p 5432:5432 postgres:17';
return $runCommand; return $runCommand;
} }
@@ -54,7 +55,7 @@ class Postgres17Driver extends DatabaseDriver
new Step( new Step(
name: 'Configure firewall', name: 'Configure firewall',
script: 'ufw allow 5432/tcp || true', script: 'ufw allow 5432/tcp || true',
) ),
]); ]);
} }
} }

View File

@@ -14,13 +14,15 @@ enum ServiceCategory: string
case STORAGE = 'storage'; case STORAGE = 'storage';
case CACHE = 'cache'; case CACHE = 'cache';
public static function getDescription(ServiceCategory|string $category) { public static function getDescription(ServiceCategory|string $category)
{
if (is_string($category)) { if (is_string($category)) {
$category = ServiceCategory::from($category); $category = ServiceCategory::from($category);
} }
if (! $category instanceof ServiceCategory) { if (! $category instanceof ServiceCategory) {
throw new \InvalidArgumentException('Invalid category provided'); throw new \InvalidArgumentException('Invalid category provided');
} }
return match ($category) { return match ($category) {
self::APPLICATION => 'The base container image for your application', self::APPLICATION => 'The base container image for your application',
self::DATABASE => 'Postgres or MySQL', self::DATABASE => 'Postgres or MySQL',

View File

@@ -4,7 +4,8 @@ namespace App\Enums;
use App\Enums\Concerns\Arrayable; use App\Enums\Concerns\Arrayable;
enum ServiceType: string { enum ServiceType: string
{
use Arrayable; use Arrayable;
case FRANKENPHP = 'frankenphp'; case FRANKENPHP = 'frankenphp';

View File

@@ -9,6 +9,7 @@ class ApplicationController extends Controller
public function show(Request $request) public function show(Request $request)
{ {
$id = $request->route('application'); $id = $request->route('application');
return inertia('applications/Show'); return inertia('applications/Show');
} }
} }

View File

@@ -10,6 +10,7 @@ class EnvironmentController extends Controller
public function show(Request $request) public function show(Request $request)
{ {
$id = $request->route('environment'); $id = $request->route('environment');
return inertia('environments/Show', [ return inertia('environments/Show', [
'environment' => Environment::findOrFail($id), 'environment' => Environment::findOrFail($id),
]); ]);

View File

@@ -2,8 +2,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request;
class OrganisationController extends Controller class OrganisationController extends Controller
{ {
public function show() public function show()

View File

@@ -8,11 +8,9 @@ use App\Enums\ServerProvider;
use App\Enums\ServerStatus; use App\Enums\ServerStatus;
use App\Jobs\Servers\WaitForServerToConnect; use App\Jobs\Servers\WaitForServerToConnect;
use App\Models\Organisation; use App\Models\Organisation;
use App\Services\ServerProviders\HetznerService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use NunoMaduro\Collision\Provider;
class ServerController extends Controller class ServerController extends Controller
{ {
@@ -35,13 +33,13 @@ class ServerController extends Controller
$providerService = app(GetProviderService::class)->execute($request->provider); $providerService = app(GetProviderService::class)->execute($request->provider);
if ($providerService) { if ($providerService) {
$locations = Cache::remember($request->provider . '.locations', now()->addHour(), function () use ($providerService) { $locations = Cache::remember($request->provider.'.locations', now()->addHour(), function () use ($providerService) {
return $providerService->getLocations(); return $providerService->getLocations();
}); });
$serverTypes = Cache::remember($request->provider . '.serverTypes', now()->addHour(), function () use ($providerService) { $serverTypes = Cache::remember($request->provider.'.serverTypes', now()->addHour(), function () use ($providerService) {
return $providerService->getServerTypes(); return $providerService->getServerTypes();
}); });
$images = Cache::remember($request->provider . '.images', now()->addHour(), function () use ($providerService) { $images = Cache::remember($request->provider.'.images', now()->addHour(), function () use ($providerService) {
return $providerService->getImages(); return $providerService->getImages();
}); });
} }
@@ -59,7 +57,7 @@ class ServerController extends Controller
$sudoPassword = Str::random(32); $sudoPassword = Str::random(32);
$providerService = app(GetProviderService::class)->execute($request->provider); $providerService = app(GetProviderService::class)->execute($request->provider);
if (!$providerService) { if (! $providerService) {
return back()->with('error', 'Invalid provider'); return back()->with('error', 'Invalid provider');
} }

View File

@@ -9,6 +9,7 @@ class ServiceController extends Controller
public function create(Request $request) public function create(Request $request)
{ {
$server = $request->route('server'); $server = $request->route('server');
return inertia('services/Create', [ return inertia('services/Create', [
'server' => $server, 'server' => $server,
]); ]);

View File

@@ -8,7 +8,7 @@ class HetznerConnector extends Connector
{ {
public function resolveBaseUrl(): string public function resolveBaseUrl(): string
{ {
return "https://api.hetzner.cloud/v1"; return 'https://api.hetzner.cloud/v1';
} }
protected function defaultHeaders(): array protected function defaultHeaders(): array
@@ -16,7 +16,7 @@ class HetznerConnector extends Connector
return [ return [
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'Accept' => 'application/json', 'Accept' => 'application/json',
'Authorization' => 'Bearer ' . config('services.hetzner.key'), 'Authorization' => 'Bearer '.config('services.hetzner.key'),
]; ];
} }
} }

View File

@@ -4,7 +4,6 @@ namespace App\Http\Integrations\Requests\Hetzner\Images;
use Saloon\Enums\Method; use Saloon\Enums\Method;
use Saloon\Http\Request; use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
class GetImagesRequest extends Request class GetImagesRequest extends Request
{ {

View File

@@ -9,7 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable; use Illuminate\Foundation\Queue\Queueable;
use Spatie\Ssh\Ssh; use Spatie\Ssh\Ssh;
class ProvisionServer implements ShouldQueue, ShouldBeEncrypted class ProvisionServer implements ShouldBeEncrypted, ShouldQueue
{ {
use Queueable; use Queueable;
@@ -37,7 +37,7 @@ class ProvisionServer implements ShouldQueue, ShouldBeEncrypted
// Download the provision script and execute it // Download the provision script and execute it
// The script will run in the background // The script will run in the background
$result = $ssh->execute([ $result = $ssh->execute([
'wget --quiet --output-document=provision.sh "' . $provisionScriptUrl . '"', 'wget --quiet --output-document=provision.sh "'.$provisionScriptUrl.'"',
'chmod +x provision.sh', 'chmod +x provision.sh',
'nohup ./provision.sh > /dev/null 2>&1 &', 'nohup ./provision.sh > /dev/null 2>&1 &',
]); ]);
@@ -46,6 +46,7 @@ class ProvisionServer implements ShouldQueue, ShouldBeEncrypted
$this->server->update([ $this->server->update([
'status' => ServerStatus::PROVISIONING_FAILED, 'status' => ServerStatus::PROVISIONING_FAILED,
]); ]);
return; return;
} }

View File

@@ -7,14 +7,14 @@ use App\Models\Server;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable; use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Str;
use Spatie\Ssh\Ssh; use Spatie\Ssh\Ssh;
class WaitForServerToConnect implements ShouldQueue, ShouldBeEncrypted class WaitForServerToConnect implements ShouldBeEncrypted, ShouldQueue
{ {
use Queueable; use Queueable;
public int $retryAfter = 15; public int $retryAfter = 15;
public int $tries = 40; public int $tries = 40;
public function __construct( public function __construct(
@@ -35,6 +35,7 @@ class WaitForServerToConnect implements ShouldQueue, ShouldBeEncrypted
if (! $process->isSuccessful()) { if (! $process->isSuccessful()) {
$this->release(15); $this->release(15);
return; return;
} }

View File

@@ -18,8 +18,7 @@ class DeployService implements ShouldQueue
public function __construct( public function __construct(
public Service $service, public Service $service,
public ?string $defaultPassword = null, public ?string $defaultPassword = null,
) ) {
{
// //
} }
@@ -27,7 +26,7 @@ class DeployService implements ShouldQueue
{ {
$driver = $this->service->driver($this->defaultPassword); $driver = $this->service->driver($this->defaultPassword);
$this->service->update([ $this->service->update([
'status' => ServiceStatus::INSTALLING 'status' => ServiceStatus::INSTALLING,
]); ]);
$this->deployment = $this->service->deployments()->create([ $this->deployment = $this->service->deployments()->create([
'status' => DeploymentStatus::PENDING, 'status' => DeploymentStatus::PENDING,

View File

@@ -7,7 +7,6 @@ use App\Enums\ServiceStatus;
use App\Models\Step; use App\Models\Step;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable; use Illuminate\Foundation\Queue\Queueable;
use Spatie\Ssh\Ssh;
class RunStep implements ShouldQueue class RunStep implements ShouldQueue
{ {
@@ -15,8 +14,7 @@ class RunStep implements ShouldQueue
public function __construct( public function __construct(
protected Step $step, protected Step $step,
) ) {
{
// //
} }
@@ -33,7 +31,7 @@ class RunStep implements ShouldQueue
$ssh = $server->sshClient() $ssh = $server->sshClient()
->onOutput(function ($output) { ->onOutput(function ($output) {
$this->step->update([ $this->step->update([
'logs' => $this->step->logs . "\n" . trim($output), 'logs' => $this->step->logs."\n".trim($output),
]); ]);
}); });
@@ -43,7 +41,7 @@ class RunStep implements ShouldQueue
$this->step->update([ $this->step->update([
'status' => DeploymentStatus::FAILED, 'status' => DeploymentStatus::FAILED,
'finished_at' => now(), 'finished_at' => now(),
'logs' => $this->step->logs . "\n" . trim($result->getErrorOutput()), 'logs' => $this->step->logs."\n".trim($result->getErrorOutput()),
]); ]);
return; return;
@@ -74,7 +72,7 @@ class RunStep implements ShouldQueue
$this->step->update([ $this->step->update([
'status' => DeploymentStatus::FAILED, 'status' => DeploymentStatus::FAILED,
'finished_at' => now(), 'finished_at' => now(),
'logs' => $this->step->logs . "\n" . trim($exception->getMessage()), 'logs' => $this->step->logs."\n".trim($exception->getMessage()),
]); ]);
$this->step->deployment->steps()->where('order', '>', $this->step->order)->update([ $this->step->deployment->steps()->where('order', '>', $this->step->order)->update([

View File

@@ -36,21 +36,21 @@ class FirewallRule extends Model
public function command(bool $delete = false): string public function command(bool $delete = false): string
{ {
$command = "ufw"; $command = 'ufw';
if ($delete) { if ($delete) {
$command .= " delete"; $command .= ' delete';
} }
if ($this->type === 'allow') { if ($this->type === 'allow') {
$command .= " allow"; $command .= ' allow';
} elseif ($this->type === 'deny') { } elseif ($this->type === 'deny') {
$command .= " deny"; $command .= ' deny';
} }
if ($this->from) { if ($this->from) {
$command .= " from {$this->from}"; $command .= " from {$this->from}";
$command .= " to any port"; $command .= ' to any port';
} }
$command .= " {$this->ports}"; $command .= " {$this->ports}";

View File

@@ -45,8 +45,9 @@ class Organisation extends Model
$slug = Str::slug($name); $slug = Str::slug($name);
$count = 2; $count = 2;
while (Organisation::where('slug', $slug)->exists()) { while (Organisation::where('slug', $slug)->exists()) {
$slug = Str::slug($name) . '-' . $count++; $slug = Str::slug($name).'-'.$count++;
} }
return $slug; return $slug;
} }
} }

View File

@@ -37,7 +37,7 @@ class Server extends Model
$server->internal_ip_ending = $existingServer $server->internal_ip_ending = $existingServer
? $existingServer->internal_ip_ending + 1 ? $existingServer->internal_ip_ending + 1
: 2; : 2;
$server->internal_ip = config('keystone.internal_ip_base') . $server->internal_ip_ending; $server->internal_ip = config('keystone.internal_ip_base').$server->internal_ip_ending;
}); });
} }

View File

@@ -41,12 +41,12 @@ class Service extends Model
public function driver( public function driver(
?string $defaultPassword = null, ?string $defaultPassword = null,
): Driver ): Driver {
{
$class = config("keystone.drivers.{$this->driver_name}"); $class = config("keystone.drivers.{$this->driver_name}");
if (!class_exists($class)) { if (! class_exists($class)) {
throw new \Exception("Driver class {$class} not found"); throw new \Exception("Driver class {$class} not found");
} }
return new $class($this->container_name, $this->container_id, defaultPassword: $defaultPassword); return new $class($this->container_name, $this->container_id, defaultPassword: $defaultPassword);
} }
} }

View File

@@ -6,7 +6,7 @@ return [
'drivers' => [ 'drivers' => [
'postgres' => [ 'postgres' => [
'17' => Postgres17Driver::class, '17' => Postgres17Driver::class,
] ],
], ],
'internal_ip_base' => env('INTERNAL_IP_BASE', '192.168.2.'), 'internal_ip_base' => env('INTERNAL_IP_BASE', '192.168.2.'),
]; ];

View File

@@ -37,6 +37,6 @@ return [
'hetzner' => [ 'hetzner' => [
'key' => env('HETZNER_KEY'), 'key' => env('HETZNER_KEY'),
] ],
]; ];

View File

@@ -20,6 +20,7 @@ class OrganisationFactory extends Factory
{ {
$name = $this->faker->company(); $name = $this->faker->company();
$owner = User::inRandomOrder()->first() ?: User::factory()->create(); $owner = User::inRandomOrder()->first() ?: User::factory()->create();
return [ return [
'name' => $this->faker->company(), 'name' => $this->faker->company(),
'slug' => Organisation::createUniqueSlug($name), 'slug' => Organisation::createUniqueSlug($name),

View File

@@ -36,7 +36,7 @@ class DatabaseSeeder extends Seeder
$organisation->servers()->saveMany($servers); $organisation->servers()->saveMany($servers);
$organisation->members()->attach($user, ['role' => OrganisationRole::ADMIN]); $organisation->members()->attach($user, ['role' => OrganisationRole::ADMIN]);
$application = $organisation->applications()->create([ $application = $organisation->applications()->create([
'name' => 'ClipBin', 'name' => 'ClipBin',
@@ -48,7 +48,7 @@ class DatabaseSeeder extends Seeder
'name' => 'Dev', 'name' => 'Dev',
'branch' => 'main', 'branch' => 'main',
'url' => 'https://dev.clipbin.hjb.dev', 'url' => 'https://dev.clipbin.hjb.dev',
'status' => 'active' 'status' => 'active',
]); ]);
} }
} }

View File

@@ -87,10 +87,11 @@ Route::post('/provision-callback', function (Request $request) {
if (! $isValidIp) { if (! $isValidIp) {
logger('someone tried to callback from an invalid IP'); logger('someone tried to callback from an invalid IP');
logger(' server ip: ' . $server->ipv4); logger(' server ip: '.$server->ipv4);
logger(' server ipv6: ' . $server->ipv6); logger(' server ipv6: '.$server->ipv6);
logger(' callback ip: ' . $request->ip()); logger(' callback ip: '.$request->ip());
logger(' server id: ' . $server->id); logger(' server id: '.$server->id);
return response('Unauthorized', 401); return response('Unauthorized', 401);
} }
@@ -101,5 +102,5 @@ Route::post('/provision-callback', function (Request $request) {
return response('OK', 200); return response('OK', 200);
})->name('provision.callback'); })->name('provision.callback');
require __DIR__ . '/settings.php'; require __DIR__.'/settings.php';
require __DIR__ . '/auth.php'; require __DIR__.'/auth.php';

View File

@@ -5,8 +5,6 @@ use App\Data\ServerProviders\CreatedServer;
use App\Models\Organisation; use App\Models\Organisation;
use App\Models\Server; use App\Models\Server;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia; use Inertia\Testing\AssertableInertia;
@@ -27,7 +25,7 @@ test('index route displays servers for an organisation', function () {
$response = $this->get(route('servers.index', ['organisation' => $organisation->id])); $response = $this->get(route('servers.index', ['organisation' => $organisation->id]));
$response->assertStatus(200); $response->assertStatus(200);
$response->assertInertia(fn(AssertableInertia $page) => $page $response->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Index')); ->component('servers/Index'));
}); });
@@ -35,7 +33,7 @@ test('create route returns inertia view', function () {
$organisation = Organisation::factory()->create(); $organisation = Organisation::factory()->create();
$response = $this->get(route('servers.create', ['organisation' => $organisation->id])); $response = $this->get(route('servers.create', ['organisation' => $organisation->id]));
$response->assertStatus(200); $response->assertStatus(200);
$response->assertInertia(fn(AssertableInertia $page) => $page $response->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Create')); ->component('servers/Create'));
}); });
@@ -67,7 +65,7 @@ test('store route creates a server with valid data', function () {
ipv6: '::1', ipv6: '::1',
status: 'running', status: 'running',
rootPassword: Str::random(16), rootPassword: Str::random(16),
) )
); );
$mock->shouldReceive('execute')->andReturn($providerMock); $mock->shouldReceive('execute')->andReturn($providerMock);
@@ -95,10 +93,10 @@ test('show route displays a single server', function () {
$response = $this->get(route('servers.show', [ $response = $this->get(route('servers.show', [
'organisation' => $organisation->id, 'organisation' => $organisation->id,
'server' => $server->id 'server' => $server->id,
])); ]));
$response->assertStatus(200); $response->assertStatus(200);
$response->assertInertia(fn(AssertableInertia $page) => $page $response->assertInertia(fn (AssertableInertia $page) => $page
->component('servers/Show')); ->component('servers/Show'));
}); });