diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12a9bdf35..53ba6c6a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -219,8 +219,13 @@ # Development Guides ## Local Development To build and run Coolify locally, see: [Development](./DEVELOPMENT.md) +### macOS Development with Lima +Mac users can use [Lima](https://lima-vm.io/) to run a lightweight Linux virtual machine for local Coolify development. This is useful if you prefer a Linux-based Docker environment on macOS. + +After creating and starting a Lima VM, run the normal local development commands from inside the VM as described in [Development](./DEVELOPMENT.md). + ## Adding a New Service To add a new one-click service, follow: https://coolify.io/docs/get-started/contribute/service ## Contributing to Documentation -To contribute to documentation, see: https://coolify.io/docs/get-started/contribute/documentation \ No newline at end of file +To contribute to documentation, see: https://coolify.io/docs/get-started/contribute/documentation diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index d14046ed1..433f544ae 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -537,6 +537,20 @@ public function loadHetznerTokens(): void ->get(); } + #[Computed] + public function limaStartCommand(): ?string + { + if (! isDev()) { + return null; + } + + return match ($this->server->uuid) { + 'lima-ubuntu-2404' => 'limactl start --yes --name=coolify-lima-ubuntu-2404 docker/lima/ubuntu-2404.yaml', + 'lima-ubuntu-2604' => 'limactl start --yes --name=coolify-lima-ubuntu-2604 docker/lima/ubuntu-2604.yaml', + default => null, + }; + } + public function searchHetznerServer(): void { $this->hetznerSearchError = null; diff --git a/database/seeders/DevelopmentRailpackExamplesSeeder.php b/database/seeders/DevelopmentRailpackExamplesSeeder.php index e736c5ecd..ebe510745 100644 --- a/database/seeders/DevelopmentRailpackExamplesSeeder.php +++ b/database/seeders/DevelopmentRailpackExamplesSeeder.php @@ -20,14 +20,33 @@ class DevelopmentRailpackExamplesSeeder extends Seeder { public const PROJECT_UUID = 'railpack-examples'; - public const ENVIRONMENT_UUID = 'railpack-examples-production'; - public const GIT_REPOSITORY = 'coollabsio/coolify-examples'; public const GIT_BRANCH = 'next'; public const REPOSITORY_PROJECT_ID = 603035348; + public const LIMA_SERVERS = [ + [ + 'server_uuid' => 'lima-ubuntu-2404', + 'server_name' => 'lima-ubuntu-2404', + 'port' => 2222, + 'environment_name' => 'ubuntu24', + 'environment_uuid' => 'railpack-examples-ubuntu24', + 'uuid_prefix' => 'ubuntu24-', + ], + [ + 'server_uuid' => 'lima-ubuntu-2604', + 'server_name' => 'lima-ubuntu-2604', + 'port' => 2223, + 'environment_name' => 'ubuntu26', + 'environment_uuid' => 'railpack-examples-ubuntu26', + 'uuid_prefix' => 'ubuntu26-', + ], + ]; + + private const LIMA_SENTINEL_URL = 'http://host.lima.internal:8000'; + public function run(): void { if (! $this->isDevelopmentEnvironment()) { @@ -37,16 +56,22 @@ public function run(): void } $this->ensureDevelopmentPrerequisitesExist(); - $destination = StandaloneDocker::query()->find(0); - if (! $destination) { + if (! StandaloneDocker::query()->find(0)) { throw new RuntimeException('StandaloneDocker with id=0 is required before running DevelopmentRailpackExamplesSeeder.'); } - $environment = $this->prepareEnvironment(); + $this->cleanupLegacyLimaProjects(); + $this->cleanupLegacyProductionExamples(); - foreach (self::examples() as $example) { - $this->upsertApplication($environment, $destination, $example); + foreach (self::LIMA_SERVERS as $limaServer) { + $this->seedEnvironment( + environmentUuid: $limaServer['environment_uuid'], + environmentName: $limaServer['environment_name'], + destination: $this->limaDestination($limaServer['server_uuid']), + uuidPrefix: $limaServer['uuid_prefix'], + nameSuffix: " ({$limaServer['environment_name']})", + ); } } @@ -440,6 +465,37 @@ private function ensureDevelopmentPrerequisitesExist(): void ], ); + foreach (self::LIMA_SERVERS as $limaServer) { + $server = Server::query()->firstOrCreate( + ['uuid' => $limaServer['server_uuid']], + [ + 'name' => $limaServer['server_name'], + 'description' => 'This is a Lima VM for local development testing', + 'ip' => 'host.docker.internal', + 'port' => $limaServer['port'], + 'team_id' => 0, + 'private_key_id' => 1, + 'proxy' => [ + 'type' => ProxyTypes::TRAEFIK->value, + 'status' => ProxyStatus::EXITED->value, + ], + ], + ); + + $server->settings->forceFill([ + 'sentinel_custom_url' => self::LIMA_SENTINEL_URL, + ])->saveQuietly(); + + StandaloneDocker::query()->firstOrCreate( + ['server_id' => $server->id], + [ + 'uuid' => "{$limaServer['server_uuid']}-docker", + 'name' => "{$limaServer['server_name']} Docker", + 'network' => 'coolify', + ], + ); + } + StandaloneDocker::query()->firstOrCreate( ['id' => 0], [ @@ -489,7 +545,71 @@ private function isDevelopmentEnvironment(): bool return in_array(config('app.env'), ['local', 'development', 'dev'], true); } - private function prepareEnvironment(): Environment + private function limaDestination(string $serverUuid): StandaloneDocker + { + $limaDestination = Server::query() + ->where('uuid', $serverUuid) + ->first() + ?->standaloneDockers() + ->first(); + + if (! $limaDestination) { + throw new RuntimeException("Lima StandaloneDocker destination is required for {$serverUuid} before running DevelopmentRailpackExamplesSeeder."); + } + + return $limaDestination; + } + + private function cleanupLegacyLimaProjects(): void + { + Project::query() + ->whereIn('uuid', [ + 'railpack-examples-lima-ubuntu-2404', + 'railpack-examples-lima-ubuntu-2604', + ]) + ->get() + ->each(function (Project $project): void { + Application::withTrashed() + ->whereIn('environment_id', $project->environments()->pluck('id')) + ->get() + ->each + ->forceDelete(); + + $project->delete(); + }); + } + + private function cleanupLegacyProductionExamples(): void + { + $project = Project::query()->where('uuid', self::PROJECT_UUID)->first(); + + if (! $project) { + return; + } + + Application::withTrashed() + ->whereIn('environment_id', $project->environments()->pluck('id')) + ->whereIn('uuid', collect(self::examples())->pluck('uuid')) + ->get() + ->each + ->forceDelete(); + } + + private function seedEnvironment( + string $environmentUuid, + string $environmentName, + StandaloneDocker $destination, + string $uuidPrefix = '', + string $nameSuffix = '', + ): void { + $environment = $this->prepareEnvironment($environmentUuid, $environmentName); + + foreach (self::examples() as $example) { + $this->upsertApplication($environment, $destination, $example, $uuidPrefix, $nameSuffix); + } + } + + private function prepareEnvironment(string $environmentUuid, string $environmentName): Environment { $project = Project::query()->firstOrNew(['uuid' => self::PROJECT_UUID]); $project->fill([ @@ -499,17 +619,29 @@ private function prepareEnvironment(): Environment ]); $project->save(); - $environment = $project->environments()->first(); + $environment = $project->environments() + ->where(function ($query) use ($environmentName, $environmentUuid): void { + $query + ->where('name', $environmentName) + ->orWhere('uuid', $environmentUuid); + }) + ->first(); + + $existingEnvironment = $project->environments()->first(); + + if (! $environment && $project->environments()->count() === 1 && $existingEnvironment?->name === 'production') { + $environment = $existingEnvironment; + } if (! $environment) { $environment = $project->environments()->create([ - 'name' => 'production', - 'uuid' => self::ENVIRONMENT_UUID, + 'name' => $environmentName, + 'uuid' => $environmentUuid, ]); } else { $environment->update([ - 'name' => 'production', - 'uuid' => self::ENVIRONMENT_UUID, + 'name' => $environmentName, + 'uuid' => $environmentUuid, ]); } @@ -519,13 +651,15 @@ private function prepareEnvironment(): Environment /** * @param array $example */ - private function upsertApplication(Environment $environment, StandaloneDocker $destination, array $example): void + private function upsertApplication(Environment $environment, StandaloneDocker $destination, array $example, string $uuidPrefix = '', string $nameSuffix = ''): void { - $application = Application::withTrashed()->firstOrNew(['uuid' => $example['uuid']]); + $uuid = $uuidPrefix.$example['uuid']; + $name = $example['name'].$nameSuffix; + $application = Application::withTrashed()->firstOrNew(['uuid' => $uuid]); $application->fill([ - 'name' => $example['name'], - 'description' => $example['name'], - 'fqdn' => "http://{$example['uuid']}.127.0.0.1.sslip.io", + 'name' => $name, + 'description' => $name, + 'fqdn' => "http://{$uuid}.127.0.0.1.sslip.io", 'repository_project_id' => $example['repository_project_id'] ?? self::REPOSITORY_PROJECT_ID, 'git_repository' => $example['git_repository'] ?? self::GIT_REPOSITORY, 'git_branch' => $example['git_branch'] ?? self::GIT_BRANCH, diff --git a/database/seeders/ProjectSeeder.php b/database/seeders/ProjectSeeder.php index ab8e54051..73ab9c530 100644 --- a/database/seeders/ProjectSeeder.php +++ b/database/seeders/ProjectSeeder.php @@ -7,6 +7,11 @@ class ProjectSeeder extends Seeder { + private const LIMA_ENVIRONMENTS = [ + ['name' => 'ubuntu24', 'uuid' => 'ubuntu24'], + ['name' => 'ubuntu26', 'uuid' => 'ubuntu26'], + ]; + public function run(): void { $project = Project::create([ @@ -16,7 +21,14 @@ public function run(): void 'team_id' => 0, ]); - // Update the auto-created environment with a deterministic UUID - $project->environments()->first()->update(['uuid' => 'production']); + foreach (self::LIMA_ENVIRONMENTS as $index => $environment) { + if ($index === 0) { + $project->environments()->first()->update($environment); + + continue; + } + + $project->environments()->create($environment); + } } } diff --git a/database/seeders/ServerSeeder.php b/database/seeders/ServerSeeder.php index 2d8746691..60b5dc317 100644 --- a/database/seeders/ServerSeeder.php +++ b/database/seeders/ServerSeeder.php @@ -9,6 +9,13 @@ class ServerSeeder extends Seeder { + private const LIMA_SENTINEL_URL = 'http://host.lima.internal:8000'; + + private const LIMA_SERVERS = [ + ['uuid' => 'lima-ubuntu-2404', 'name' => 'lima-ubuntu-2404', 'port' => 2222], + ['uuid' => 'lima-ubuntu-2604', 'name' => 'lima-ubuntu-2604', 'port' => 2223], + ]; + public function run(): void { Server::create([ @@ -24,5 +31,25 @@ public function run(): void 'status' => ProxyStatus::EXITED->value, ], ]); + + foreach (self::LIMA_SERVERS as $limaServer) { + $server = Server::create([ + 'uuid' => $limaServer['uuid'], + 'name' => $limaServer['name'], + 'description' => 'This is a Lima VM for local development testing', + 'ip' => 'host.docker.internal', + 'port' => $limaServer['port'], + 'team_id' => 0, + 'private_key_id' => 1, + 'proxy' => [ + 'type' => ProxyTypes::TRAEFIK->value, + 'status' => ProxyStatus::EXITED->value, + ], + ]); + + $server->settings->forceFill([ + 'sentinel_custom_url' => self::LIMA_SENTINEL_URL, + ])->saveQuietly(); + } } } diff --git a/docker-compose-maxio.dev.yml b/docker-compose-maxio.dev.yml index bbb483d7a..61037391b 100644 --- a/docker-compose-maxio.dev.yml +++ b/docker-compose-maxio.dev.yml @@ -10,6 +10,8 @@ services: - GROUP_ID=${GROUPID:-1000} ports: - "${APP_PORT:-8000}:8080" + extra_hosts: + - "host.docker.internal:host-gateway" environment: AUTORUN_ENABLED: false PUSHER_HOST: "${PUSHER_HOST}" @@ -70,6 +72,8 @@ services: ports: - "${FORWARD_SOKETI_PORT:-6001}:6001" - "6002:6002" + extra_hosts: + - "host.docker.internal:host-gateway" volumes: - ./storage:/var/www/html/storage - ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9c93678af..8f84b5d60 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -10,6 +10,8 @@ services: - GROUP_ID=${GROUPID:-1000} ports: - "${APP_PORT:-8000}:8080" + extra_hosts: + - "host.docker.internal:host-gateway" environment: AUTORUN_ENABLED: false PUSHER_HOST: "${PUSHER_HOST}" @@ -70,6 +72,8 @@ services: ports: - "${FORWARD_SOKETI_PORT:-6001}:6001" - "6002:6002" + extra_hosts: + - "host.docker.internal:host-gateway" volumes: - ./storage:/var/www/html/storage - ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js diff --git a/docker/lima/ubuntu-2404.yaml b/docker/lima/ubuntu-2404.yaml new file mode 100644 index 000000000..99819938d --- /dev/null +++ b/docker/lima/ubuntu-2404.yaml @@ -0,0 +1,35 @@ +images: + - location: https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img + arch: x86_64 + - location: https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img + arch: aarch64 + +cpus: 2 +memory: 2GiB +disk: 20GiB + +containerd: + system: false + user: false + +mounts: [] + +ssh: + localPort: 2222 + loadDotSSHPubKeys: false + +provision: + - mode: system + script: | + #!/bin/bash + set -euxo pipefail + + install -d -m 700 /root/.ssh + cat >/root/.ssh/authorized_keys <<'EOF' + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 sail@76ff66d2e2dd + EOF + chmod 600 /root/.ssh/authorized_keys + + sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config + sed -i 's/^#\?PubkeyAuthentication .*/PubkeyAuthentication yes/' /etc/ssh/sshd_config + systemctl restart ssh diff --git a/docker/lima/ubuntu-2604.yaml b/docker/lima/ubuntu-2604.yaml new file mode 100644 index 000000000..042e3028a --- /dev/null +++ b/docker/lima/ubuntu-2604.yaml @@ -0,0 +1,35 @@ +images: + - location: https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-amd64.img + arch: x86_64 + - location: https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-arm64.img + arch: aarch64 + +cpus: 2 +memory: 2GiB +disk: 20GiB + +containerd: + system: false + user: false + +mounts: [] + +ssh: + localPort: 2223 + loadDotSSHPubKeys: false + +provision: + - mode: system + script: | + #!/bin/bash + set -euxo pipefail + + install -d -m 700 /root/.ssh + cat >/root/.ssh/authorized_keys <<'EOF' + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 sail@76ff66d2e2dd + EOF + chmod 600 /root/.ssh/authorized_keys + + sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config + sed -i 's/^#\?PubkeyAuthentication .*/PubkeyAuthentication yes/' /etc/ssh/sshd_config + systemctl restart ssh diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index cfbeccd0c..e5ee310a2 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -120,6 +120,19 @@ class="flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded bg-warn @else You can't use this server until it is validated. @endif + @if ($this->limaStartCommand) +
+
Start this Lima VM locally
+
+ Run this from the Coolify repository root before validating this server: +
+ + {{ $this->limaStartCommand }} + +
+ @endif @if ($isValidating)
diff --git a/tests/Feature/DevelopmentRailpackExamplesSeederTest.php b/tests/Feature/DevelopmentRailpackExamplesSeederTest.php index 321b8e52b..00042f837 100644 --- a/tests/Feature/DevelopmentRailpackExamplesSeederTest.php +++ b/tests/Feature/DevelopmentRailpackExamplesSeederTest.php @@ -17,6 +17,7 @@ use Database\Seeders\TeamSeeder; use Database\Seeders\UserSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Collection; uses(RefreshDatabase::class); @@ -33,6 +34,11 @@ function seedRailpackExamplePrerequisites(): void ]); } +function limaServers(): Collection +{ + return collect(DevelopmentRailpackExamplesSeeder::LIMA_SERVERS); +} + it('can seed the railpack examples directly on a clean development database', function () { config()->set('app.env', 'local'); @@ -44,14 +50,51 @@ function seedRailpackExamplePrerequisites(): void expect(StandaloneDocker::query()->find(0))->not->toBeNull(); expect(GithubApp::query()->find(0))->not->toBeNull(); expect(GitlabApp::query()->find(1))->not->toBeNull(); - expect(Project::query()->where('uuid', DevelopmentRailpackExamplesSeeder::PROJECT_UUID)->exists())->toBeTrue(); - expect(Application::query()->count())->toBe(count(DevelopmentRailpackExamplesSeeder::examples())); + expect(Application::query()->count())->toBe(count(DevelopmentRailpackExamplesSeeder::examples()) * limaServers()->count()); + + $project = Project::query() + ->where('uuid', DevelopmentRailpackExamplesSeeder::PROJECT_UUID) + ->first(); + + expect($project) + ->not->toBeNull() + ->and($project->environments)->toHaveCount(limaServers()->count()); + + foreach (limaServers() as $limaServer) { + $server = Server::query()->where('uuid', $limaServer['server_uuid'])->first(); + + expect($server)->not->toBeNull(); + expect($server->settings->sentinel_custom_url)->toBe('http://host.lima.internal:8000'); + expect(StandaloneDocker::query()->whereRelation('server', 'uuid', $limaServer['server_uuid'])->exists())->toBeTrue(); + expect($project->environments()->where('uuid', $limaServer['environment_uuid'])->exists())->toBeTrue(); + } }); it('seeds the railpack examples in development mode', function () { config()->set('app.env', 'local'); seedRailpackExamplePrerequisites(); + $legacyProject = Project::query()->create([ + 'uuid' => 'railpack-examples-lima-ubuntu-2404', + 'name' => 'Railpack Examples - lima-ubuntu-2404', + 'description' => 'Legacy generated Railpack examples project', + 'team_id' => 0, + ]); + Application::query()->create([ + 'uuid' => 'lima-ubuntu-2404-railpack-nextjs-ssr', + 'name' => 'Legacy Railpack Next.js SSR Example', + 'repository_project_id' => DevelopmentRailpackExamplesSeeder::REPOSITORY_PROJECT_ID, + 'git_repository' => DevelopmentRailpackExamplesSeeder::GIT_REPOSITORY, + 'git_branch' => DevelopmentRailpackExamplesSeeder::GIT_BRANCH, + 'build_pack' => 'railpack', + 'ports_exposes' => '3000', + 'environment_id' => $legacyProject->environments()->first()->id, + 'destination_id' => 0, + 'destination_type' => StandaloneDocker::class, + 'source_id' => 0, + 'source_type' => GithubApp::class, + ]); + $this->seed(DevelopmentRailpackExamplesSeeder::class); $project = Project::query() @@ -61,30 +104,58 @@ function seedRailpackExamplePrerequisites(): void expect($project) ->not->toBeNull() ->and($project->name)->toBe('Railpack Examples') - ->and($project->environments)->toHaveCount(1) - ->and($project->environments->first()->uuid)->toBe(DevelopmentRailpackExamplesSeeder::ENVIRONMENT_UUID); + ->and($project->environments)->toHaveCount(limaServers()->count()); + expect(Project::query()->pluck('uuid')->sort()->values()->all())->toBe([ + 'project', + DevelopmentRailpackExamplesSeeder::PROJECT_UUID, + ]); + expect(Application::query()->where('uuid', 'lima-ubuntu-2404-railpack-nextjs-ssr')->exists())->toBeFalse(); $applications = $project->applications()->with('settings')->orderBy('uuid')->get(); - expect($applications)->toHaveCount(count(DevelopmentRailpackExamplesSeeder::examples())); + expect($applications)->toHaveCount(count(DevelopmentRailpackExamplesSeeder::examples()) * limaServers()->count()); expect($applications->every(fn (Application $application) => $application->build_pack === 'railpack'))->toBeTrue(); + $examples = collect(DevelopmentRailpackExamplesSeeder::examples())->keyBy('uuid'); expect($applications->every( - fn (Application $application) => $application->git_repository === ($examples->get($application->uuid)['git_repository'] ?? DevelopmentRailpackExamplesSeeder::GIT_REPOSITORY) + fn (Application $application) => $application->git_repository === ($examples->get(str($application->uuid)->after('-')->value())['git_repository'] ?? DevelopmentRailpackExamplesSeeder::GIT_REPOSITORY) ))->toBeTrue(); expect($applications->every( - fn (Application $application) => $application->git_branch === ($examples->get($application->uuid)['git_branch'] ?? DevelopmentRailpackExamplesSeeder::GIT_BRANCH) + fn (Application $application) => $application->git_branch === ($examples->get(str($application->uuid)->after('-')->value())['git_branch'] ?? DevelopmentRailpackExamplesSeeder::GIT_BRANCH) ))->toBeTrue(); - $nestjs = $applications->firstWhere('uuid', 'railpack-nestjs'); - $angularStatic = $applications->firstWhere('uuid', 'railpack-angular-static'); - $eleventyStatic = $applications->firstWhere('uuid', 'railpack-eleventy-static'); - $pythonFlask = $applications->firstWhere('uuid', 'railpack-python-flask'); - $goGin = $applications->firstWhere('uuid', 'railpack-go-gin'); - $rust = $applications->firstWhere('uuid', 'railpack-rust'); - $githubDeployKey = $applications->firstWhere('uuid', 'railpack-github-deploy-key'); - $gitlabDeployKey = $applications->firstWhere('uuid', 'railpack-gitlab-deploy-key'); - $gitlabPublic = $applications->firstWhere('uuid', 'railpack-gitlab-public-example'); + foreach (limaServers() as $limaServer) { + $limaEnvironment = $project->environments() + ->where('uuid', $limaServer['environment_uuid']) + ->first(); + + expect($limaEnvironment) + ->not->toBeNull() + ->and($limaEnvironment->name)->toBe($limaServer['environment_name']); + + $limaApplications = $limaEnvironment->applications()->with('settings', 'destination.server')->orderBy('uuid')->get(); + + expect($limaApplications)->toHaveCount(count(DevelopmentRailpackExamplesSeeder::examples())); + expect($limaApplications->every(fn (Application $application) => $application->build_pack === 'railpack'))->toBeTrue(); + expect($limaApplications->every(fn (Application $application) => str($application->uuid)->startsWith($limaServer['uuid_prefix'])))->toBeTrue(); + expect($limaApplications->every(fn (Application $application) => $application->destination->server->uuid === $limaServer['server_uuid']))->toBeTrue(); + expect($limaApplications->every( + fn (Application $application) => $application->git_repository === ($examples->get(str($application->uuid)->after($limaServer['uuid_prefix'])->value())['git_repository'] ?? DevelopmentRailpackExamplesSeeder::GIT_REPOSITORY) + ))->toBeTrue(); + expect($limaApplications->every( + fn (Application $application) => $application->git_branch === ($examples->get(str($application->uuid)->after($limaServer['uuid_prefix'])->value())['git_branch'] ?? DevelopmentRailpackExamplesSeeder::GIT_BRANCH) + ))->toBeTrue(); + } + + $nestjs = $applications->firstWhere('uuid', 'ubuntu24-railpack-nestjs'); + $angularStatic = $applications->firstWhere('uuid', 'ubuntu24-railpack-angular-static'); + $eleventyStatic = $applications->firstWhere('uuid', 'ubuntu24-railpack-eleventy-static'); + $pythonFlask = $applications->firstWhere('uuid', 'ubuntu24-railpack-python-flask'); + $goGin = $applications->firstWhere('uuid', 'ubuntu24-railpack-go-gin'); + $rust = $applications->firstWhere('uuid', 'ubuntu24-railpack-rust'); + $githubDeployKey = $applications->firstWhere('uuid', 'ubuntu24-railpack-github-deploy-key'); + $gitlabDeployKey = $applications->firstWhere('uuid', 'ubuntu24-railpack-gitlab-deploy-key'); + $gitlabPublic = $applications->firstWhere('uuid', 'ubuntu24-railpack-gitlab-public-example'); expect($nestjs) ->not->toBeNull() @@ -156,6 +227,12 @@ function seedRailpackExamplePrerequisites(): void expect(Project::query()->where('uuid', DevelopmentRailpackExamplesSeeder::PROJECT_UUID)->exists())->toBeFalse(); expect(Application::query()->where('uuid', 'railpack-nextjs-ssr')->exists())->toBeFalse(); + + foreach (limaServers() as $limaServer) { + expect(Project::query()->where('uuid', 'railpack-examples-lima-ubuntu-2404')->exists())->toBeFalse(); + expect(Project::query()->where('uuid', 'railpack-examples-lima-ubuntu-2604')->exists())->toBeFalse(); + expect(Application::query()->where('uuid', $limaServer['uuid_prefix'].'railpack-nextjs-ssr')->exists())->toBeFalse(); + } }); it('is idempotent when run multiple times', function () { @@ -170,5 +247,14 @@ function seedRailpackExamplePrerequisites(): void ->first(); expect($project)->not->toBeNull(); - expect($project->applications()->count())->toBe(count(DevelopmentRailpackExamplesSeeder::examples())); + expect($project->applications()->count())->toBe(count(DevelopmentRailpackExamplesSeeder::examples()) * limaServers()->count()); + + foreach (limaServers() as $limaServer) { + $limaEnvironment = $project->environments() + ->where('uuid', $limaServer['environment_uuid']) + ->first(); + + expect($limaEnvironment)->not->toBeNull(); + expect($limaEnvironment->applications()->count())->toBe(count(DevelopmentRailpackExamplesSeeder::examples())); + } }); diff --git a/tests/Feature/ProjectSeederTest.php b/tests/Feature/ProjectSeederTest.php new file mode 100644 index 000000000..bce4c2d69 --- /dev/null +++ b/tests/Feature/ProjectSeederTest.php @@ -0,0 +1,31 @@ +seed([ + UserSeeder::class, + TeamSeeder::class, + PrivateKeySeeder::class, + ProjectSeeder::class, + ]); + + $project = Project::query() + ->where('uuid', 'project') + ->first(); + + expect($project) + ->not->toBeNull() + ->and($project->name)->toBe('My first project') + ->and($project->environments()->pluck('uuid', 'name')->all())->toBe([ + 'ubuntu24' => 'ubuntu24', + 'ubuntu26' => 'ubuntu26', + ]); +}); diff --git a/tests/Feature/ServerLimaStartCommandTest.php b/tests/Feature/ServerLimaStartCommandTest.php new file mode 100644 index 000000000..2d89cbe46 --- /dev/null +++ b/tests/Feature/ServerLimaStartCommandTest.php @@ -0,0 +1,96 @@ +create([ + 'id' => 0, + 'is_registration_enabled' => true, + ]); + }); + + $this->user = User::factory()->create(); + $this->team = Team::factory()->create(['show_boarding' => false]); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); + + $this->privateKey = PrivateKey::create([ + 'team_id' => $this->team->id, + 'name' => 'Test Key', + 'private_key' => '-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk +hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA +AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV +uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== +-----END OPENSSH PRIVATE KEY-----', + ]); +}); + +it('shows the lima start command on lima server general pages in development', function () { + config()->set('app.env', 'local'); + + foreach (limaStartCommandDefinitions() as $definition) { + $server = createServerForLimaStartCommandTest($definition['uuid'], $definition['port']); + + $this->get(route('server.show', ['server_uuid' => $server->uuid])) + ->assertSuccessful() + ->assertSee('Start this Lima VM locally') + ->assertSee($definition['command']); + } +}); + +it('does not show the lima start command outside development', function () { + config()->set('app.env', 'testing'); + + $server = createServerForLimaStartCommandTest('lima-ubuntu-2404', 2222); + + $this->get(route('server.show', ['server_uuid' => $server->uuid])) + ->assertSuccessful() + ->assertDontSee('Start this Lima VM locally') + ->assertDontSee('limactl start --yes --name=coolify-lima-ubuntu-2404 docker/lima/ubuntu-2404.yaml'); +}); + +function createServerForLimaStartCommandTest(string $uuid, int $port): Server +{ + return Server::factory()->create([ + 'uuid' => $uuid, + 'name' => $uuid, + 'ip' => 'host.docker.internal', + 'port' => $port, + 'team_id' => test()->team->id, + 'private_key_id' => test()->privateKey->id, + 'proxy' => [ + 'type' => ProxyTypes::TRAEFIK->value, + 'status' => ProxyStatus::EXITED->value, + ], + ]); +} + +function limaStartCommandDefinitions(): array +{ + return [ + [ + 'uuid' => 'lima-ubuntu-2404', + 'port' => 2222, + 'command' => 'limactl start --yes --name=coolify-lima-ubuntu-2404 docker/lima/ubuntu-2404.yaml', + ], + [ + 'uuid' => 'lima-ubuntu-2604', + 'port' => 2223, + 'command' => 'limactl start --yes --name=coolify-lima-ubuntu-2604 docker/lima/ubuntu-2604.yaml', + ], + ]; +} diff --git a/tests/Feature/ServerSeederTest.php b/tests/Feature/ServerSeederTest.php new file mode 100644 index 000000000..022c33b74 --- /dev/null +++ b/tests/Feature/ServerSeederTest.php @@ -0,0 +1,66 @@ + 'lima-ubuntu-2404', 'port' => 2222, 'template' => 'ubuntu-2404.yaml', 'memory' => '2GiB', 'disk' => '20GiB'], + ['uuid' => 'lima-ubuntu-2604', 'port' => 2223, 'template' => 'ubuntu-2604.yaml', 'memory' => '2GiB', 'disk' => '20GiB'], + ]; +} + +it('seeds the development testing host and lima servers', function () { + $this->seed([ + UserSeeder::class, + TeamSeeder::class, + PrivateKeySeeder::class, + ServerSeeder::class, + ]); + + $testingHost = Server::query()->where('uuid', 'localhost')->first(); + + expect($testingHost) + ->not->toBeNull() + ->and($testingHost->ip)->toBe('coolify-testing-host'); + + foreach (limaServerDefinitions() as $definition) { + $limaServer = Server::query()->where('uuid', $definition['uuid'])->first(); + + expect($limaServer) + ->not->toBeNull() + ->and($limaServer->name)->toBe($definition['uuid']) + ->and($limaServer->ip)->toBe('host.docker.internal') + ->and($limaServer->port)->toBe($definition['port']) + ->and($limaServer->user)->toBe('root') + ->and($limaServer->team_id)->toBe(0) + ->and($limaServer->private_key_id)->toBe(1) + ->and($limaServer->settings)->not->toBeNull() + ->and($limaServer->settings->sentinel_custom_url)->toBe('http://host.lima.internal:8000') + ->and($limaServer->destinations())->toHaveCount(1); + } +}); + +it('keeps the lima templates aligned with the seeded servers', function () { + foreach (limaServerDefinitions() as $definition) { + $template = Yaml::parseFile(base_path("docker/lima/{$definition['template']}")); + $script = data_get($template, 'provision.0.script'); + + expect(data_get($template, 'ssh.localPort')) + ->toBe($definition['port']) + ->and(data_get($template, 'memory'))->toBe($definition['memory']) + ->and(data_get($template, 'disk'))->toBe($definition['disk']) + ->and(data_get($template, 'containerd.system'))->toBeFalse() + ->and(data_get($template, 'containerd.user'))->toBeFalse() + ->and(data_get($template, 'networks'))->toBeNull() + ->and($script)->toContain('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68'); + } +});