diff --git a/.instructions/project-refactor.md b/.instructions/project-refactor.md new file mode 100644 index 0000000..c0f5e8d --- /dev/null +++ b/.instructions/project-refactor.md @@ -0,0 +1,11 @@ +The project's structure is changing. The current setup is Applications have Environments and those environments have slices that belong to them. + +We're no longer doing this. The application is changing to be more server-centric. + +Slices and Environments can be removed. + +Servers are going to have services, which can be selected on server provision. Once the server has finished provisioning, we run deployments for all of the services the user has selected for that server. + +Applications can be installed on a server. The application could be installed across multiple servers, and deployed at the same time. The application isn't tied directly to the server, instead it has an Instance model that acts as an intermediary. This means that the user can deploy an Application which will update both instances. + +Create a plan for refactoring the project to support this new structure. \ No newline at end of file diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..8c6715a --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": [ + "artisan", + "boost:mcp" + ] + } + } +} \ No newline at end of file diff --git a/app/Actions/Applications/CreateInstance.php b/app/Actions/Applications/CreateInstance.php new file mode 100644 index 0000000..59df609 --- /dev/null +++ b/app/Actions/Applications/CreateInstance.php @@ -0,0 +1,24 @@ +instances()->create([ + 'server_id' => $server->id, + 'branch' => $branch, + 'status' => 'pending', + 'config' => $config, + ]); + } +} \ No newline at end of file diff --git a/app/Data/Slices/CaddySliceData.php b/app/Data/Slices/CaddySliceData.php deleted file mode 100644 index 8fe7bb5..0000000 --- a/app/Data/Slices/CaddySliceData.php +++ /dev/null @@ -1,12 +0,0 @@ -findOrFail($request->route('organisation')); + $organisation = Organisation::with('applications.instances.server')->findOrFail($request->route('organisation')); return inertia('applications/Index', [ 'applications' => $organisation->applications, @@ -19,7 +21,18 @@ class ApplicationController extends Controller public function show(Request $request) { $id = $request->route('application'); + $application = Application::with(['instances.server', 'organisation'])->findOrFail($id); - return inertia('applications/Show'); + return inertia('applications/Show', [ + 'application' => $application, + 'servers' => inertia()->optional(function () use ($application) { + return $application + ->organisation + ?->servers() + ->where('status', ServerStatus::ACTIVE) + ->with('services') + ->get() ?? []; + }), + ]); } } diff --git a/app/Http/Controllers/EnvironmentController.php b/app/Http/Controllers/EnvironmentController.php deleted file mode 100644 index 1b943fd..0000000 --- a/app/Http/Controllers/EnvironmentController.php +++ /dev/null @@ -1,29 +0,0 @@ -route('environment'); - $environment = Environment::with('application')->findOrFail($id); - - return inertia('environments/Show', [ - 'environment' => $environment, - 'servers' => inertia()->optional(function () use ($environment) { - return $environment - ->application - ?->organisation - ?->servers() - ->where('status', ServerStatus::ACTIVE) - ->with('services') - ->get() ?? []; - }), - ]); - } -} diff --git a/app/Http/Controllers/InstanceController.php b/app/Http/Controllers/InstanceController.php new file mode 100644 index 0000000..0765332 --- /dev/null +++ b/app/Http/Controllers/InstanceController.php @@ -0,0 +1,36 @@ +validate([ + 'server_id' => 'required|exists:servers,id', + 'branch' => 'required|string|max:255', + 'config' => 'sometimes|array', + ]); + + $server = Server::findOrFail($validated['server_id']); + + $instance = (new CreateInstance())->execute( + $application, + $server, + $validated['branch'], + $validated['config'] ?? [] + ); + + return redirect() + ->route('applications.show', [ + 'organisation' => $application->organisation_id, + 'application' => $application->id + ]) + ->with('success', 'Instance created successfully'); + } +} \ No newline at end of file diff --git a/app/Jobs/Applications/DeployApplication.php b/app/Jobs/Applications/DeployApplication.php new file mode 100644 index 0000000..dd73757 --- /dev/null +++ b/app/Jobs/Applications/DeployApplication.php @@ -0,0 +1,59 @@ +deployment = $this->application->deployments()->create([ + 'status' => DeploymentStatus::PENDING, + ]); + + foreach ($this->application->instances as $instance) { + $step = $this->deployment->steps()->create([ + 'name' => "Deploy to {$instance->server->name}", + 'order' => $instance->id, + 'status' => DeploymentStatus::PENDING, + 'script' => $this->getDeploymentScript($instance), + 'secrets' => [], + ]); + + $step->dispatchJob(); + } + } + + protected function getDeploymentScript($instance): string + { + return "#!/bin/bash\n" . + "cd /opt/apps/{$this->application->name}-{$instance->id}\n" . + "git fetch origin\n" . + "git checkout {$instance->branch}\n" . + "git pull origin {$instance->branch}\n"; + } + + public function failed(\Throwable $exception): void + { + if (isset($this->deployment)) { + $this->deployment->update([ + 'status' => DeploymentStatus::FAILED, + ]); + } + } +} \ No newline at end of file diff --git a/app/Models/Application.php b/app/Models/Application.php index 684c440..e97a08e 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -6,6 +6,8 @@ use App\Enums\RepositoryType; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\Relations\MorphMany; class Application extends Model { @@ -23,8 +25,18 @@ class Application extends Model return $this->belongsTo(Organisation::class); } - public function environments(): HasMany + public function instances(): HasMany { - return $this->hasMany(Environment::class); + return $this->hasMany(Instance::class); + } + + public function servers(): HasManyThrough + { + return $this->hasManyThrough(Server::class, Instance::class); + } + + public function deployments(): MorphMany + { + return $this->morphMany(Deployment::class, 'target'); } } diff --git a/app/Models/Environment.php b/app/Models/Environment.php deleted file mode 100644 index 8acd72f..0000000 --- a/app/Models/Environment.php +++ /dev/null @@ -1,28 +0,0 @@ -belongsTo(Application::class); - } - - public function slices(): HasMany - { - return $this->hasMany(Slice::class); - } - - public function services(): HasManyThrough - { - return $this->hasManyThrough(Service::class, Slice::class); - } -} diff --git a/app/Models/Instance.php b/app/Models/Instance.php new file mode 100644 index 0000000..4b25bfa --- /dev/null +++ b/app/Models/Instance.php @@ -0,0 +1,34 @@ + 'array', + ]; + } + + public function application(): BelongsTo + { + return $this->belongsTo(Application::class); + } + + public function server(): BelongsTo + { + return $this->belongsTo(Server::class); + } + + public function deployments(): MorphMany + { + return $this->morphMany(Deployment::class, 'target'); + } +} \ No newline at end of file diff --git a/app/Models/Server.php b/app/Models/Server.php index 9804557..47432cf 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -44,6 +44,16 @@ class Server extends Model return $this->hasMany(Service::class); } + public function instances(): HasMany + { + return $this->hasMany(Instance::class); + } + + public function applications(): HasManyThrough + { + return $this->hasManyThrough(Application::class, Instance::class); + } + public function firewallRules(): HasMany { return $this->hasMany(FirewallRule::class); @@ -64,14 +74,14 @@ class Server extends Model )->where('target_type', (new Service)->getMorphClass()); } - public function environmentDeployments(): HasManyThrough + public function applicationDeployments(): HasManyThrough { return $this->hasManyThrough( Deployment::class, - Environment::class, + Application::class, 'server_id', 'target_id', - )->where('target_type', (new Environment)->getMorphClass()); + )->where('target_type', (new Application)->getMorphClass()); } public function sshClient(string $user = 'root'): Ssh diff --git a/app/Models/Service.php b/app/Models/Service.php index 84221be..1a5d140 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -10,7 +10,6 @@ use App\Enums\ServiceType; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphMany; class Service extends Model @@ -41,11 +40,6 @@ class Service extends Model return $this->belongsTo(Server::class); } - public function slices(): HasMany - { - return $this->hasMany(Slice::class); - } - public function deployments(): MorphMany { return $this->morphMany(Deployment::class, 'target'); diff --git a/app/Models/Slice.php b/app/Models/Slice.php deleted file mode 100644 index cb6a83b..0000000 --- a/app/Models/Slice.php +++ /dev/null @@ -1,16 +0,0 @@ -belongsTo(Service::class); - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index a974c74..6304829 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,12 +4,11 @@ namespace App\Providers; use App\Models\Application; use App\Models\Deployment; -use App\Models\Environment; +use App\Models\Instance; use App\Models\Organisation; use App\Models\OrganisationUser; use App\Models\Server; use App\Models\Service; -use App\Models\Slice; use App\Models\Step; use App\Models\User; use Illuminate\Database\Eloquent\Relations\Relation; @@ -33,12 +32,11 @@ class AppServiceProvider extends ServiceProvider Relation::enforceMorphMap([ 'application' => Application::class, 'deployment' => Deployment::class, - 'environment' => Environment::class, + 'instance' => Instance::class, 'organisation' => Organisation::class, 'organisation-user' => OrganisationUser::class, 'server' => Server::class, 'service' => Service::class, - 'slice' => Slice::class, 'step' => Step::class, 'user' => User::class, ]); diff --git a/composer.json b/composer.json index f37af37..484df4f 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ }, "require-dev": { "fakerphp/faker": "^1.23", + "laravel/boost": "^1.1", "laravel/pail": "^1.2.2", "laravel/pint": "^1.18", "laravel/sail": "^1.41", diff --git a/composer.lock b/composer.lock index 67eeb9e..fe26904 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0ad069ff3461e30a7d92cfea145f68e7", + "content-hash": "69f6de114270a8beb46d9283a2acd24d", "packages": [ { "name": "brick/math", @@ -6600,6 +6600,135 @@ }, "time": "2025-03-19T14:43:43+00:00" }, + { + "name": "laravel/boost", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/boost.git", + "reference": "70f909465bf73dad7e791fad8b7716b3b2712076" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/boost/zipball/70f909465bf73dad7e791fad8b7716b3b2712076", + "reference": "70f909465bf73dad7e791fad8b7716b3b2712076", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "laravel/mcp": "^0.1.1", + "laravel/prompts": "^0.1.9|^0.3", + "laravel/roster": "^0.2.5", + "php": "^8.1" + }, + "require-dev": { + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "pestphp/pest": "^2.0|^3.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Boost\\BoostServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Boost\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laravel Boost accelerates AI-assisted development to generate high-quality, Laravel-specific code.", + "homepage": "https://github.com/laravel/boost", + "keywords": [ + "ai", + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/boost/issues", + "source": "https://github.com/laravel/boost" + }, + "time": "2025-09-04T12:16:09+00:00" + }, + { + "name": "laravel/mcp", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/mcp.git", + "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/mcp/zipball/6d6284a491f07c74d34f48dfd999ed52c567c713", + "reference": "6d6284a491f07c74d34f48dfd999ed52c567c713", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/validation": "^10.0|^11.0|^12.0", + "php": "^8.1|^8.2" + }, + "require-dev": { + "laravel/pint": "^1.14", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" + }, + "providers": [ + "Laravel\\Mcp\\Server\\McpServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Mcp\\": "src/", + "Workbench\\App\\": "workbench/app/", + "Laravel\\Mcp\\Tests\\": "tests/", + "Laravel\\Mcp\\Server\\": "src/Server/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The easiest way to add MCP servers to your Laravel app.", + "homepage": "https://github.com/laravel/mcp", + "keywords": [ + "dev", + "laravel", + "mcp" + ], + "support": { + "issues": "https://github.com/laravel/mcp/issues", + "source": "https://github.com/laravel/mcp" + }, + "time": "2025-08-16T09:50:43+00:00" + }, { "name": "laravel/pail", "version": "v1.2.2", @@ -6744,6 +6873,67 @@ }, "time": "2025-03-14T22:31:42+00:00" }, + { + "name": "laravel/roster", + "version": "v0.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/roster.git", + "reference": "5615acdf860c5a5c61d04aba44f2d3312550c514" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/roster/zipball/5615acdf860c5a5c61d04aba44f2d3312550c514", + "reference": "5615acdf860c5a5c61d04aba44f2d3312550c514", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1|^8.2", + "symfony/yaml": "^6.4|^7.2" + }, + "require-dev": { + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.22.0|^9.0|^10.0", + "pestphp/pest": "^2.0|^3.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Roster\\RosterServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Roster\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect packages & approaches in use within a Laravel project", + "homepage": "https://github.com/laravel/roster", + "keywords": [ + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/roster/issues", + "source": "https://github.com/laravel/roster" + }, + "time": "2025-09-04T07:31:39+00:00" + }, { "name": "laravel/sail", "version": "v1.41.0", diff --git a/database/migrations/2025_03_27_122033_create_slices_table.php b/database/migrations/2025_03_27_122033_create_slices_table.php deleted file mode 100644 index 7f5fad9..0000000 --- a/database/migrations/2025_03_27_122033_create_slices_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->foreignIdFor(Service::class); - $table->foreignIdFor(Environment::class); - $table->json('data')->nullable(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('slices'); - } -}; diff --git a/database/migrations/2025_03_27_120315_create_environments_table.php b/database/migrations/2025_03_27_122034_create_instances_table.php similarity index 68% rename from database/migrations/2025_03_27_120315_create_environments_table.php rename to database/migrations/2025_03_27_122034_create_instances_table.php index 23ad163..226b808 100644 --- a/database/migrations/2025_03_27_120315_create_environments_table.php +++ b/database/migrations/2025_03_27_122034_create_instances_table.php @@ -1,6 +1,7 @@ id(); $table->foreignIdFor(Application::class); - $table->string('name'); + $table->foreignIdFor(Server::class); $table->string('branch'); - $table->string('url'); $table->string('status'); + $table->json('config')->nullable(); $table->timestamps(); }); } public function down(): void { - Schema::dropIfExists('environments'); + Schema::dropIfExists('instances'); } -}; +}; \ No newline at end of file diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..64dafe7 --- /dev/null +++ b/opencode.json @@ -0,0 +1,8 @@ +{ + "tools": { + "laravel-boost": { + "command": "php", + "args": ["artisan", "boost:mcp"] + } + } +} diff --git a/resources/js/pages/applications/Show.vue b/resources/js/pages/applications/Show.vue index 1022df8..9aa277c 100644 --- a/resources/js/pages/applications/Show.vue +++ b/resources/js/pages/applications/Show.vue @@ -3,7 +3,7 @@ import { Badge } from '@/components/ui/badge'; import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import AppLayout from '@/layouts/AppLayout.vue'; import { Head, Link } from '@inertiajs/vue3'; -import { Layers2Icon } from 'lucide-vue-next'; +import { ServerIcon } from 'lucide-vue-next'; const props = defineProps({ application: { @@ -38,43 +38,35 @@ const props = defineProps({
-

Environments

+

Server Instances

- +
- - + +
- {{ environment.name }} - {{ environment.status.replace('-', ' ') }} + + {{ instance.server.name }} + {{ + instance.status.replace('-', ' ') + }}
- - {{ environment.type }} {{ environment.version }} - + Branch: {{ instance.branch }}
- {{ application }} diff --git a/resources/js/pages/environments/Show.vue b/resources/js/pages/environments/Show.vue deleted file mode 100644 index 5fdaf0d..0000000 --- a/resources/js/pages/environments/Show.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - diff --git a/resources/js/pages/organisations/Show.vue b/resources/js/pages/organisations/Show.vue index 5d1a87b..94fd890 100644 --- a/resources/js/pages/organisations/Show.vue +++ b/resources/js/pages/organisations/Show.vue @@ -21,11 +21,14 @@ const tabValue = ref(new URL(window.location.href).hash?.replace('#', '') || 'da watch(tabValue, () => { window.history.pushState({}, '', `#${tabValue.value}`); }); -watch(() => window.location.hash, (newHash) => { - if (newHash) { - tabValue.value = newHash.replace('#', ''); - } -}); +watch( + () => window.location.hash, + (newHash) => { + if (newHash) { + tabValue.value = newHash.replace('#', ''); + } + }, +);