New direction; removed wireguard, readme update

This commit is contained in:
2025-09-07 11:37:52 +01:00
parent 82556535ba
commit a91780d1d5
21 changed files with 102 additions and 5622 deletions

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Actions\Servers;
use App\Models\Server;
use Spatie\QueueableAction\QueueableAction;
class SyncUfwRules
{
use QueueableAction;
public function execute(
Server $server,
) {
$ssh = $server->sshClient();
$result = $ssh->execute('wg show wg0');
if (! $result->isSuccessful()) {
logger()->error('Failed to retrieve WireGuard rules', [
'server_id' => $server->id,
'error' => $result->getErrorOutput(),
]);
throw new \Exception('Failed to retrieve WireGuard rules');
}
}
}

View File

@@ -1,58 +0,0 @@
<?php
namespace App\Actions\Servers;
use App\Models\Server;
use Illuminate\Support\Str;
use Spatie\QueueableAction\QueueableAction;
class SyncWireguardRules
{
use QueueableAction;
public function execute(
Server $server,
) {
$ssh = $server->sshClient();
$result = $ssh->execute('wg show wg0');
if (! $result->isSuccessful()) {
logger()->error('Failed to retrieve WireGuard rules', [
'server_id' => $server->id,
'error' => $result->getErrorOutput(),
]);
throw new \Exception('Failed to retrieve WireGuard rules');
}
$output = $result->getOutput();
$commands = collect();
$server->organisation->servers()->where('id', '!=', $server->id)->each(function ($organisationServer) use (&$commands, $output, $server) {
if (Str::contains($output, $organisationServer->internal_public_key)) {
$commands->push("wg set wg0 peer {$organisationServer->internal_public_key} remove");
}
if ($organisationServer->external_network_id === $server->external_network_id) {
$commands->push("wg set wg0 peer {$organisationServer->internal_public_key} allowed-ips {$organisationServer->internal_ip}/32");
} else {
$commands->push("wg set wg0 peer {$organisationServer->internal_public_key} allowed-ips {$organisationServer->ipv4}/32,{$organisationServer->ipv6}/128");
}
});
$result = $ssh->execute($commands->toArray());
if (! $result->isSuccessful()) {
logger()->error('Failed to sync WireGuard rules', [
'server_id' => $server->id,
'error' => $result->getErrorOutput(),
]);
throw new \Exception('Failed to sync WireGuard rules');
}
logger()->info('Successfully synced WireGuard rules', [
'server_id' => $server->id,
'commands' => $commands->toArray(),
'output' => $result->getOutput(),
]);
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Enums;
use App\Enums\Concerns\Arrayable;
enum NetworkType: string
{
use Arrayable;
case EXTERNAL = 'external'; // managed by provider
case INTERNAL = 'internal'; // managed by keystone
}

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Servers\SyncWireguardRules;
use App\Enums\ServerStatus; use App\Enums\ServerStatus;
use App\Events\Servers\ServerProvisioned; use App\Events\Servers\ServerProvisioned;
use App\Models\Server; use App\Models\Server;
@@ -15,7 +14,6 @@ class ProvisionCallback extends Controller
{ {
$validated = $request->validate([ $validated = $request->validate([
'server_id' => ['required', 'integer', 'exists:servers,id'], 'server_id' => ['required', 'integer', 'exists:servers,id'],
'internal_public_key' => ['required', 'string'],
]); ]);
$server = Server::find($validated['server_id']); $server = Server::find($validated['server_id']);
@@ -41,11 +39,11 @@ class ProvisionCallback extends Controller
$server->update([ $server->update([
'status' => ServerStatus::ACTIVE, 'status' => ServerStatus::ACTIVE,
'internal_public_key' => $validated['internal_public_key'],
]); ]);
$server->organisation->servers()->each(function ($s) { $server->organisation->servers()->each(function ($s) {
app(SyncWireguardRules::class)->onQueue()->execute($s); // app(SyncWireguardRules::class)->onQueue()->execute($s);
// @todo change this to a sync ufw rules class
}); });
event(new ServerProvisioned($server)); event(new ServerProvisioned($server));

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\GenerateRandomSlug; use App\Actions\GenerateRandomSlug;
use App\Enums\NetworkType;
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;
@@ -93,7 +92,6 @@ class ServerController extends Controller
$network = $provider->networks()->create([ $network = $provider->networks()->create([
'organisation_id' => $provider->organisation_id, 'organisation_id' => $provider->organisation_id,
'external_id' => $createdNetwork->id, 'external_id' => $createdNetwork->id,
'type' => NetworkType::EXTERNAL,
'name' => $createdNetwork->name, 'name' => $createdNetwork->name,
'ip_range' => $createdNetwork->ipRange, 'ip_range' => $createdNetwork->ipRange,
'network_zone' => $networkZone, 'network_zone' => $networkZone,
@@ -123,7 +121,7 @@ class ServerController extends Controller
'os' => $request->image, 'os' => $request->image,
'plan' => $request->server_type, 'plan' => $request->server_type,
'user' => 'keystone', 'user' => 'keystone',
'external_network_id' => $network->id, 'network_id' => $network->id,
]); ]);
dispatch(new WaitForServerToConnect( dispatch(new WaitForServerToConnect(

View File

@@ -2,7 +2,6 @@
namespace App\Models; namespace App\Models;
use App\Enums\NetworkType;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -13,19 +12,12 @@ class Network extends Model
protected function casts(): array protected function casts(): array
{ {
return [ return [];
'type' => NetworkType::class,
];
} }
public function internalServers(): HasMany public function servers(): HasMany
{ {
return $this->hasMany(Server::class, 'internal_network_id'); return $this->hasMany(Server::class, 'network_id');
}
public function externalServers(): HasMany
{
return $this->hasMany(Server::class, 'external_network_id');
} }
public function organisation(): BelongsTo public function organisation(): BelongsTo

View File

@@ -27,27 +27,11 @@ class Server extends Model
public static function boot(): void public static function boot(): void
{ {
parent::boot(); parent::boot();
static::creating(function (self $server) {
$existingServer = Server::whereOrganisationId($server->organisation_id)
->orderByDesc('internal_ip_ending')
->first();
$server->internal_ip_ending = $existingServer
? $existingServer->internal_ip_ending + 1
: 2;
$server->internal_ip = config('keystone.internal_ip_base') . $server->internal_ip_ending;
});
} }
public function externalNetwork(): BelongsTo public function network(): BelongsTo
{ {
return $this->belongsTo(Network::class, 'external_network_id'); return $this->belongsTo(Network::class, 'network');
}
public function internalNetwork(): BelongsTo
{
return $this->belongsTo(Network::class, 'internal_network_id');
} }
public function organisation(): BelongsTo public function organisation(): BelongsTo

BIN
bun.lockb Normal file → Executable file

Binary file not shown.

View File

@@ -14,7 +14,6 @@ return [
'2' => Caddy2Driver::class, '2' => Caddy2Driver::class,
] ]
], ],
'internal_ip_base' => env('INTERNAL_IP_BASE', '192.168.2.'),
'services' => [ 'services' => [
ServiceCategory::DATABASE->value => [ ServiceCategory::DATABASE->value => [

View File

@@ -35,7 +35,7 @@ class ServerFactory extends Factory
{ {
return $this->state(function (array $attributes) use ($networkId) { return $this->state(function (array $attributes) use ($networkId) {
return [ return [
'external_network_id' => $networkId, 'network_id' => $networkId,
]; ];
}); });
} }

View File

@@ -14,8 +14,7 @@ return new class extends Migration
Schema::create('servers', function (Blueprint $table) { Schema::create('servers', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignIdFor(Organisation::class); $table->foreignIdFor(Organisation::class);
$table->foreignIdFor(Network::class, 'external_network_id'); $table->foreignIdFor(Network::class);
$table->foreignIdFor(Network::class, 'internal_network_id')->nullable();
$table->foreignIdFor(Provider::class); $table->foreignIdFor(Provider::class);
$table->string('external_id')->nullable(); $table->string('external_id')->nullable();
$table->string('name'); $table->string('name');
@@ -23,9 +22,6 @@ return new class extends Migration
$table->string('ipv6'); $table->string('ipv6');
$table->string('private_ip'); $table->string('private_ip');
$table->string('provider_status'); $table->string('provider_status');
$table->string('internal_ip');
$table->integer('internal_ip_ending');
$table->text('internal_public_key')->nullable();
$table->string('status'); $table->string('status');
$table->string('region'); $table->string('region');
$table->string('os'); $table->string('os');

View File

@@ -16,7 +16,6 @@ return new class extends Migration
$table->foreignIdFor(Provider::class); $table->foreignIdFor(Provider::class);
$table->string('external_id')->nullable(); $table->string('external_id')->nullable();
$table->string('network_zone')->default('global'); $table->string('network_zone')->default('global');
$table->string('type');
$table->string('name'); $table->string('name');
$table->string('ip_range'); $table->string('ip_range');
$table->timestamps(); $table->timestamps();

View File

@@ -2,7 +2,6 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Enums\NetworkType;
use App\Enums\OrganisationRole; use App\Enums\OrganisationRole;
use App\Enums\ProviderType; use App\Enums\ProviderType;
use App\Enums\RepositoryType; use App\Enums\RepositoryType;
@@ -42,7 +41,6 @@ class DatabaseSeeder extends Seeder
if (! app()->isProduction()) { if (! app()->isProduction()) {
$network = $organisation->networks()->create([ $network = $organisation->networks()->create([
'type' => NetworkType::EXTERNAL,
'name' => 'keystone', 'name' => 'keystone',
'external_id' => 'net-12345', 'external_id' => 'net-12345',
'provider_id' => $provider->id, 'provider_id' => $provider->id,

5408
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,13 +24,13 @@
}, },
"dependencies": { "dependencies": {
"@inertiajs/vue3": "^2.0.0-beta.3", "@inertiajs/vue3": "^2.0.0-beta.3",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^6.0.1",
"@vueuse/core": "^12.0.0", "@vueuse/core": "^12.0.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"laravel-vite-plugin": "^1.0", "laravel-vite-plugin": "^2.0.1",
"lucide": "^0.468.0", "lucide": "^0.468.0",
"lucide-vue-next": "^0.468.0", "lucide-vue-next": "^0.468.0",
"radix-vue": "^1.9.11", "radix-vue": "^1.9.11",
@@ -39,7 +39,7 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^6.2.0", "vite": "npm:rolldown-vite@latest",
"vue": "^3.5.13", "vue": "^3.5.13",
"ziggy-js": "^2.4.2" "ziggy-js": "^2.4.2"
}, },

View File

@@ -4,7 +4,6 @@
# [server_id!] - the servers id # [server_id!] - the servers id
# [keystonepublickey!] - keystone's public key # [keystonepublickey!] - keystone's public key
# [callback!] - callback url # [callback!] - callback url
# [internal_ip_ending!] - internal ip ending
apt_wait() { apt_wait() {
while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do
@@ -35,7 +34,7 @@ apt update
apt_wait apt_wait
apt upgrade -y apt upgrade -y
apt_wait apt_wait
apt install unzip curl fail2ban ufw wireguard -y apt install unzip curl fail2ban ufw -y
# No password logins # No password logins
sed -i "/PasswordAuthentication yes/d" /etc/ssh/sshd_config sed -i "/PasswordAuthentication yes/d" /etc/ssh/sshd_config
@@ -52,11 +51,6 @@ if [ ! -d /root/.ssh ]; then
touch /root/.ssh/authorized_keys touch /root/.ssh/authorized_keys
fi fi
# Create the wireguard directory
if [ ! -d /root/.wg ]; then
mkdir -p /root/.wg
fi
# Set The Hostname If Necessary # Set The Hostname If Necessary
echo "[!hostname!]" > /etc/hostname sed -i 's/127\.0\.0\.1.*localhost/127.0.0.1 [!hostname!].localdomain [!hostname!] localhost/' /etc/hosts echo "[!hostname!]" > /etc/hostname sed -i 's/127\.0\.0\.1.*localhost/127.0.0.1 [!hostname!].localdomain [!hostname!] localhost/' /etc/hosts
hostname [!hostname!] hostname [!hostname!]
@@ -65,7 +59,6 @@ hostname [!hostname!]
useradd keystone useradd keystone
mkdir -p /home/keystone/.ssh mkdir -p /home/keystone/.ssh
mkdir -p /home/keystone/.keystone mkdir -p /home/keystone/.keystone
mkdir -p /home/keystone/.wg
adduser keystone sudo adduser keystone sudo
# Setup Bash For Keystone User # Setup Bash For Keystone User
@@ -91,16 +84,6 @@ ssh-keygen -f /home/keystone/.ssh/id_ed25519 -t ed25519 -N ''
# Restart SSH # Restart SSH
service ssh restart service ssh restart
# Create the wireguard key pairs
wg genkey > /root/.wg/privatekey
wg pubkey < /root/.wg/privatekey > /root/.wg/publickey
# Configure wireguard
ip link add dev wg0 type wireguard
ip address add dev wg0 192.168.2.[!internal_ip_ending!]/24
wg set wg0 listen-port 51820 private-key /root/.wg/privatekey
ip link set up dev wg0
# Setup Keystone Home Directory Permissions # Setup Keystone Home Directory Permissions
chown -R keystone:keystone /home/keystone chown -R keystone:keystone /home/keystone
chmod -R 755 /home/keystone chmod -R 755 /home/keystone
@@ -108,7 +91,6 @@ chmod 700 /home/keystone/.ssh/id_rsa
# Setup UFW Firewall # Setup UFW Firewall
ufw allow 22 ufw allow 22
ufw allow 51820 # wireguard
ufw --force enable ufw --force enable
# Add Keystone User To www-data Group # Add Keystone User To www-data Group
@@ -168,7 +150,5 @@ APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1"; APT::Periodic::Unattended-Upgrade "1";
EOF EOF
INTERNAL_PUBLIC_KEY="$(cat /root/.wg/publickey)"
# Callback that the server is installed # Callback that the server is installed
curl --insecure --data "server_id=[!server_id!]&internal_public_key=$INTERNAL_PUBLIC_KEY" [!callback!] curl --insecure --data "server_id=[!server_id!]" [!callback!]

View File

@@ -1,6 +1,6 @@
# Keystone # Keystone
Keystone is an opinionated Laravel deployment tool. Think of it as a middle-ground between Forge and Cloud, with Envoyer built in. Laravel Forge, but running with Docker instead of raw services on servers. Also zero downtime built in, ideally with the option for a dedicated build server as well as building on the server itself. (start with the latter)
## STUFF ## STUFF
@@ -8,8 +8,9 @@ MAKE SURE TO INSTALL sshpass on the server this is running on
## Overview ## Overview
Every application has a gateway (just a load balancer), regardless of how many app servers it's running. - Each server should have a gateway (reverse proxy) at the front. This is a service, but there should only be _one_ allowed per server.
We're going to install wireguard on each server to provide a secure connection between every server and manage internal connections via the firewall with ufw. - Service table should probably have a json column of ports that are used by the docker service (ones passed onto the host net - not internal docker ones) so we can check for conflicts before installing new services.
For each server provider, we should create a private network on that provider to get the lowest latency, which means allocating the wireguard connections needs to be done intelligently. If the server provider is not the same, we should use the public IP, otherwise use the private one internally.
If a server is created on a provider, we should create the 'keystone' network. Maybe search to see if it already exists first.
## Networking Model
ufw man.

View File

@@ -1,7 +0,0 @@
// This is a generated file.
export default {
"EXTERNAL": "external",
"INTERNAL": "internal"
}

View File

@@ -8,18 +8,18 @@ test('registration screen can be rendered', function () {
$response->assertStatus(200); $response->assertStatus(200);
}); });
test('new users can register', function () { // test('new users can register', function () {
$response = $this->post('/register', [ // $response = $this->post('/register', [
'name' => 'Test User', // 'name' => 'Test User',
'email' => 'test@example.com', // 'email' => 'test@example.com',
'password' => 'password', // 'password' => 'password',
'password_confirmation' => 'password', // 'password_confirmation' => 'password',
]); // ]);
$this->assertAuthenticated(); // $this->assertAuthenticated();
assertTrue(auth()->user()->organisations()->count() === 1); // assertTrue(auth()->user()->organisations()->count() === 1);
assertTrue(auth()->user()->ownedOrganisations()->count() === 1); // assertTrue(auth()->user()->ownedOrganisations()->count() === 1);
$response->assertRedirect(route('dashboard', absolute: false)); // $response->assertRedirect(route('dashboard', absolute: false));
}); // });

View File

@@ -1,7 +1,6 @@
<?php <?php
use App\Data\ServerProviders\CreatedServer; use App\Data\ServerProviders\CreatedServer;
use App\Enums\NetworkType;
use App\Enums\ProviderType; use App\Enums\ProviderType;
use App\Models\Organisation; use App\Models\Organisation;
use App\Models\Provider; use App\Models\Provider;
@@ -28,7 +27,6 @@ test('index route displays servers for an organisation', function () {
$organisation = Organisation::factory()->create(); $organisation = Organisation::factory()->create();
$provider = Provider::factory()->forOrganisation($organisation->id)->create(); $provider = Provider::factory()->forOrganisation($organisation->id)->create();
$network = $organisation->networks()->create([ $network = $organisation->networks()->create([
'type' => NetworkType::EXTERNAL,
'name' => 'keystone', 'name' => 'keystone',
'external_id' => 'net-12345', 'external_id' => 'net-12345',
'provider_id' => $provider->id, 'provider_id' => $provider->id,
@@ -38,7 +36,7 @@ test('index route displays servers for an organisation', function () {
Server::factory()->count(2)->create([ Server::factory()->count(2)->create([
'provider_id' => $provider->id, 'provider_id' => $provider->id,
'organisation_id' => $organisation->id, 'organisation_id' => $organisation->id,
'external_network_id' => $network->id, 'network_id' => $network->id,
]); ]);
$response = $this->get(route('servers.index', ['organisation' => $organisation->id])); $response = $this->get(route('servers.index', ['organisation' => $organisation->id]));
@@ -81,7 +79,6 @@ test('store route creates a server with valid data', function () {
]); ]);
$network = $organisation->networks()->create([ $network = $organisation->networks()->create([
'type' => NetworkType::EXTERNAL,
'name' => 'keystone', 'name' => 'keystone',
'external_id' => 'net-12345', 'external_id' => 'net-12345',
'provider_id' => $provider->id, 'provider_id' => $provider->id,
@@ -118,7 +115,7 @@ test('store route creates a server with valid data', function () {
'provider_id' => $provider->id, 'provider_id' => $provider->id,
'region' => 'hel1', 'region' => 'hel1',
'os' => 'ubuntu-20.04', 'os' => 'ubuntu-20.04',
'external_network_id' => $network->id, 'network_id' => $network->id,
]); ]);
}); });
@@ -126,7 +123,6 @@ test('show route displays a single server', function () {
$organisation = Organisation::factory()->create(); $organisation = Organisation::factory()->create();
$provider = Provider::factory()->forOrganisation($organisation)->create(); $provider = Provider::factory()->forOrganisation($organisation)->create();
$network = $organisation->networks()->create([ $network = $organisation->networks()->create([
'type' => NetworkType::EXTERNAL,
'name' => 'keystone', 'name' => 'keystone',
'external_id' => 'net-12345', 'external_id' => 'net-12345',
'provider_id' => $provider->id, 'provider_id' => $provider->id,
@@ -134,7 +130,7 @@ test('show route displays a single server', function () {
]); ]);
$server = Server::factory()->create([ $server = Server::factory()->create([
'organisation_id' => $organisation->id, 'organisation_id' => $organisation->id,
'external_network_id' => $network->id, 'network_id' => $network->id,
'provider_id' => $provider->id, 'provider_id' => $provider->id,
]); ]);

View File

@@ -2,8 +2,6 @@
use App\Actions\Services\CreateService; use App\Actions\Services\CreateService;
use App\Drivers\Driver; use App\Drivers\Driver;
use App\Drivers\Postgres\Postgres17Driver;
use App\Enums\NetworkType;
use App\Enums\ServiceCategory; use App\Enums\ServiceCategory;
use App\Enums\ServiceStatus; use App\Enums\ServiceStatus;
use App\Enums\ServiceType; use App\Enums\ServiceType;
@@ -21,21 +19,21 @@ use Illuminate\Support\Facades\Bus;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
function setupTestEnvironment() { function setupTestEnvironment()
{
$user = User::factory()->create(); $user = User::factory()->create();
$organisation = Organisation::factory()->create([ $organisation = Organisation::factory()->create([
'owner_id' => $user->id 'owner_id' => $user->id
]); ]);
$provider = Provider::factory()->create([ $provider = Provider::factory()->create([
'organisation_id' => $organisation->id 'organisation_id' => $organisation->id
]); ]);
$network = Network::create([ $network = Network::create([
'name' => 'test-network', 'name' => 'test-network',
'ip_range' => '10.0.0.0/24', 'ip_range' => '10.0.0.0/24',
'type' => NetworkType::EXTERNAL,
'external_id' => 'ext-12345', 'external_id' => 'ext-12345',
'organisation_id' => $organisation->id, 'organisation_id' => $organisation->id,
'provider_id' => $provider->id, 'provider_id' => $provider->id,
@@ -44,7 +42,7 @@ function setupTestEnvironment() {
$server = Server::factory()->create([ $server = Server::factory()->create([
'organisation_id' => $organisation->id, 'organisation_id' => $organisation->id,
'provider_id' => $provider->id, 'provider_id' => $provider->id,
'external_network_id' => $network->id, 'network_id' => $network->id,
]); ]);
return [ return [
@@ -58,41 +56,42 @@ function setupTestEnvironment() {
test('create service page is accessible', function () { test('create service page is accessible', function () {
$setup = setupTestEnvironment(); $setup = setupTestEnvironment();
$this->actingAs($setup['user']); $this->actingAs($setup['user']);
$response = $this->get(route('services.create', [ $response = $this->get(route('services.create', [
'organisation' => $setup['organisation']->id, 'organisation' => $setup['organisation']->id,
'server' => $setup['server']->id 'server' => $setup['server']->id
])); ]));
$response->assertStatus(200); $response->assertStatus(200);
$response->assertInertia(fn (AssertableInertia $page) => $page $response->assertInertia(
->component('services/Create') fn(AssertableInertia $page) => $page
->has('server') ->component('services/Create')
->has('services') ->has('server')
->has('services')
); );
}); });
test('store service with valid data', function () { test('store service with valid data', function () {
$setup = setupTestEnvironment(); $setup = setupTestEnvironment();
$this->actingAs($setup['user']); $this->actingAs($setup['user']);
$mockDefaultCredentials = [ $mockDefaultCredentials = [
'user' => 'test-user', 'user' => 'test-user',
'password' => 'test-password', 'password' => 'test-password',
'db' => 'test-db' 'db' => 'test-db'
]; ];
$mockDriver = Mockery::mock(Driver::class); $mockDriver = Mockery::mock(Driver::class);
$mockDriver->shouldReceive('defaultCredentials')->andReturn($mockDefaultCredentials); $mockDriver->shouldReceive('defaultCredentials')->andReturn($mockDefaultCredentials);
// intercept the driver // intercept the driver
$this->partialMock(Service::class, function ($mock) use ($mockDriver) { $this->partialMock(Service::class, function ($mock) use ($mockDriver) {
$mock->shouldReceive('driver')->andReturn($mockDriver); $mock->shouldReceive('driver')->andReturn($mockDriver);
}); });
Bus::fake(); Bus::fake();
$data = [ $data = [
@@ -113,7 +112,7 @@ test('store service with valid data', function () {
'server' => $setup['server']->id 'server' => $setup['server']->id
])); ]));
$response->assertSessionHas('success', 'Service created successfully'); $response->assertSessionHas('success', 'Service created successfully');
$this->assertDatabaseHas('services', [ $this->assertDatabaseHas('services', [
'name' => 'test-postgres-database', 'name' => 'test-postgres-database',
'server_id' => $setup['server']->id, 'server_id' => $setup['server']->id,
@@ -123,13 +122,13 @@ test('store service with valid data', function () {
'driver_name' => 'postgres.17', 'driver_name' => 'postgres.17',
'status' => ServiceStatus::NOT_INSTALLED->value, 'status' => ServiceStatus::NOT_INSTALLED->value,
]); ]);
Bus::assertDispatched(DeployService::class); Bus::assertDispatched(DeployService::class);
}); });
test('store service with invalid data', function () { test('store service with invalid data', function () {
$setup = setupTestEnvironment(); $setup = setupTestEnvironment();
$this->actingAs($setup['user']); $this->actingAs($setup['user']);
$data = [ $data = [
@@ -149,9 +148,9 @@ test('store service with invalid data', function () {
test('store service validates version exists in config', function () { test('store service validates version exists in config', function () {
$setup = setupTestEnvironment(); $setup = setupTestEnvironment();
$this->actingAs($setup['user']); $this->actingAs($setup['user']);
// Mock the config to simulate the version not existing // Mock the config to simulate the version not existing
Config::set('keystone.services.' . ServiceCategory::DATABASE->value . '.' . ServiceType::POSTGRES->value . '.versions', [ Config::set('keystone.services.' . ServiceCategory::DATABASE->value . '.' . ServiceType::POSTGRES->value . '.versions', [
'16' => [ '16' => [
@@ -160,7 +159,7 @@ test('store service validates version exists in config', function () {
'image' => 'postgres:16', 'image' => 'postgres:16',
] ]
]); ]);
$data = [ $data = [
'name' => 'test-postgres-database', 'name' => 'test-postgres-database',
'category' => ServiceCategory::DATABASE->value, 'category' => ServiceCategory::DATABASE->value,
@@ -172,15 +171,15 @@ test('store service validates version exists in config', function () {
'organisation' => $setup['organisation']->id, 'organisation' => $setup['organisation']->id,
'server' => $setup['server']->id 'server' => $setup['server']->id
]), $data); ]), $data);
$response->assertSessionHasErrors(['version']); $response->assertSessionHasErrors(['version']);
}); });
test('store service with non-existent server returns 404', function () { test('store service with non-existent server returns 404', function () {
$setup = setupTestEnvironment(); $setup = setupTestEnvironment();
$this->actingAs($setup['user']); $this->actingAs($setup['user']);
$data = [ $data = [
'name' => 'test-postgres-database', 'name' => 'test-postgres-database',
'category' => ServiceCategory::DATABASE->value, 'category' => ServiceCategory::DATABASE->value,
@@ -192,27 +191,27 @@ test('store service with non-existent server returns 404', function () {
'organisation' => $setup['organisation']->id, 'organisation' => $setup['organisation']->id,
'server' => 9999 'server' => 9999
]), $data); ]), $data);
$response->assertStatus(404); $response->assertStatus(404);
}); });
test('create service page with non-existent server returns 404', function () { test('create service page with non-existent server returns 404', function () {
$setup = setupTestEnvironment(); $setup = setupTestEnvironment();
$this->actingAs($setup['user']); $this->actingAs($setup['user']);
$response = $this->get(route('services.create', [ $response = $this->get(route('services.create', [
'organisation' => $setup['organisation']->id, 'organisation' => $setup['organisation']->id,
'server' => 9999 'server' => 9999
])); ]));
$response->assertStatus(404); $response->assertStatus(404);
}); });
test('store service is properly created and dispatched', function () { test('store service is properly created and dispatched', function () {
$setup = setupTestEnvironment(); $setup = setupTestEnvironment();
$this->actingAs($setup['user']); $this->actingAs($setup['user']);
// Setup mock credentials and driver // Setup mock credentials and driver
$mockDriver = Mockery::mock(Driver::class)->shouldReceive('defaultCredentials') $mockDriver = Mockery::mock(Driver::class)->shouldReceive('defaultCredentials')
->andReturn([ ->andReturn([
@@ -221,7 +220,7 @@ test('store service is properly created and dispatched', function () {
'db' => 'test-db' 'db' => 'test-db'
]) ])
->getMock(); ->getMock();
// Setup test data // Setup test data
$testData = [ $testData = [
'name' => 'test-postgres-database', 'name' => 'test-postgres-database',
@@ -229,12 +228,12 @@ test('store service is properly created and dispatched', function () {
'type' => ServiceType::POSTGRES->value, 'type' => ServiceType::POSTGRES->value,
'version' => '17', 'version' => '17',
]; ];
// Mock service class to return our mock driver // Mock service class to return our mock driver
$this->partialMock(Service::class, function ($mock) use ($mockDriver) { $this->partialMock(Service::class, function ($mock) use ($mockDriver) {
$mock->shouldReceive('driver')->andReturn($mockDriver); $mock->shouldReceive('driver')->andReturn($mockDriver);
}); });
// Mock CreateService action // Mock CreateService action
$this->mock(CreateService::class, function ($mock) use ($setup, $testData) { $this->mock(CreateService::class, function ($mock) use ($setup, $testData) {
$service = new Service([ $service = new Service([
@@ -247,9 +246,9 @@ test('store service is properly created and dispatched', function () {
'driver_name' => 'postgres.17', 'driver_name' => 'postgres.17',
'status' => ServiceStatus::NOT_INSTALLED, 'status' => ServiceStatus::NOT_INSTALLED,
]); ]);
$service->setRelation('server', $setup['server']); $service->setRelation('server', $setup['server']);
$mock->shouldReceive('execute') $mock->shouldReceive('execute')
->once() ->once()
->withArgs(function ($server, $name, $category, $type, $version) use ($setup, $testData) { ->withArgs(function ($server, $name, $category, $type, $version) use ($setup, $testData) {
@@ -261,7 +260,7 @@ test('store service is properly created and dispatched', function () {
}) })
->andReturn($service); ->andReturn($service);
}); });
Bus::fake(); Bus::fake();
// Execute request // Execute request
@@ -287,7 +286,7 @@ test('store service is properly created and dispatched', function () {
'driver_name' => 'postgres.17', 'driver_name' => 'postgres.17',
'status' => ServiceStatus::NOT_INSTALLED->value, 'status' => ServiceStatus::NOT_INSTALLED->value,
]); ]);
// Assert job was dispatched // Assert job was dispatched
Bus::assertDispatched(DeployService::class); Bus::assertDispatched(DeployService::class);
}); });