Implement Keystone environment deployments
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Applications;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Instance;
|
||||
use App\Models\Server;
|
||||
|
||||
class CreateInstance
|
||||
{
|
||||
public function execute(
|
||||
Application $application,
|
||||
Server $server,
|
||||
string $branch = 'main',
|
||||
array $config = []
|
||||
): Instance {
|
||||
return $application->instances()->create([
|
||||
'server_id' => $server->id,
|
||||
'branch' => $branch,
|
||||
'status' => 'pending',
|
||||
'config' => $config,
|
||||
]);
|
||||
}
|
||||
}
|
||||
65
app/Actions/Applications/CreateLaravelEnvironment.php
Normal file
65
app/Actions/Applications/CreateLaravelEnvironment.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Applications;
|
||||
|
||||
use App\Enums\DeployPolicy;
|
||||
use App\Enums\SchedulerMode;
|
||||
use App\Enums\ServiceCategory;
|
||||
use App\Enums\ServiceStatus;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
|
||||
class CreateLaravelEnvironment
|
||||
{
|
||||
public function execute(
|
||||
Application $application,
|
||||
string $name,
|
||||
?string $branch = null,
|
||||
string $phpVersion = '8.4',
|
||||
): Environment {
|
||||
$environment = $application->environments()->create([
|
||||
'name' => $name,
|
||||
'branch' => $branch ?? $application->default_branch,
|
||||
'status' => 'pending',
|
||||
'scheduler_enabled' => true,
|
||||
'scheduler_mode' => SchedulerMode::SINGLE,
|
||||
'build_config' => [
|
||||
'php_version' => $phpVersion,
|
||||
'document_root' => 'public',
|
||||
'health_path' => '/up',
|
||||
'js_build_command' => null,
|
||||
'js_package_manager' => 'bun',
|
||||
],
|
||||
]);
|
||||
|
||||
$web = $environment->services()->create([
|
||||
'organisation_id' => $application->organisation_id,
|
||||
'name' => 'web',
|
||||
'category' => ServiceCategory::APPLICATION,
|
||||
'type' => ServiceType::LARAVEL,
|
||||
'version' => "php-{$phpVersion}",
|
||||
'version_track' => "php-{$phpVersion}",
|
||||
'driver_name' => "laravel.php-{$phpVersion}",
|
||||
'status' => ServiceStatus::NOT_INSTALLED,
|
||||
'desired_replicas' => 1,
|
||||
'deploy_policy' => DeployPolicy::WITH_ENVIRONMENT,
|
||||
'process_roles' => ['web', 'scheduler'],
|
||||
'config' => [
|
||||
'migration_mode' => 'auto',
|
||||
'migration_timing' => 'pre_switch',
|
||||
'migration_command' => 'php artisan migrate --force',
|
||||
'document_root' => 'public',
|
||||
'health_path' => '/up',
|
||||
'js_build_command' => null,
|
||||
'js_package_manager' => 'bun',
|
||||
],
|
||||
]);
|
||||
|
||||
$environment->forceFill([
|
||||
'scheduler_target_service_id' => $web->id,
|
||||
])->save();
|
||||
|
||||
return $environment->refresh();
|
||||
}
|
||||
}
|
||||
78
app/Actions/Applications/GenerateDeployKey.php
Normal file
78
app/Actions/Applications/GenerateDeployKey.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Applications;
|
||||
|
||||
use App\Models\Application;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class GenerateDeployKey
|
||||
{
|
||||
/**
|
||||
* @param array{public: string, private: string, fingerprint?: string}|null $keyPair
|
||||
*/
|
||||
public function execute(Application $application, ?array $keyPair = null): Application
|
||||
{
|
||||
$keyPair ??= $this->generateWithSshKeygen($application);
|
||||
|
||||
$application->forceFill([
|
||||
'deploy_key_public' => $keyPair['public'],
|
||||
'deploy_key_private' => $keyPair['private'],
|
||||
'deploy_key_fingerprint' => $keyPair['fingerprint'] ?? $this->fingerprint($keyPair['public']),
|
||||
'deploy_key_installed_at' => null,
|
||||
])->save();
|
||||
|
||||
return $application->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{public: string, private: string, fingerprint: string}
|
||||
*/
|
||||
private function generateWithSshKeygen(Application $application): array
|
||||
{
|
||||
$directory = storage_path('app/private/deploy-keys/'.str()->uuid()->toString());
|
||||
$privateKeyPath = $directory.'/id_ed25519';
|
||||
|
||||
File::ensureDirectoryExists($directory, 0700);
|
||||
|
||||
try {
|
||||
$result = Process::run([
|
||||
'ssh-keygen',
|
||||
'-t',
|
||||
'ed25519',
|
||||
'-C',
|
||||
"keystone-application-{$application->id}",
|
||||
'-N',
|
||||
'',
|
||||
'-f',
|
||||
$privateKeyPath,
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new RuntimeException('Unable to generate deploy key: '.$result->errorOutput());
|
||||
}
|
||||
|
||||
return [
|
||||
'public' => trim(File::get($privateKeyPath.'.pub')),
|
||||
'private' => trim(File::get($privateKeyPath)),
|
||||
'fingerprint' => $this->fingerprint(trim(File::get($privateKeyPath.'.pub'))),
|
||||
];
|
||||
} finally {
|
||||
rescue(fn () => File::deleteDirectory($directory), report: false);
|
||||
}
|
||||
}
|
||||
|
||||
private function fingerprint(string $publicKey): string
|
||||
{
|
||||
try {
|
||||
$parts = explode(' ', trim($publicKey));
|
||||
$keyMaterial = $parts[1] ?? $publicKey;
|
||||
|
||||
return 'SHA256:'.rtrim(strtr(base64_encode(hash('sha256', base64_decode($keyMaterial, true) ?: $publicKey, true)), '+/', '-_'), '=');
|
||||
} catch (Throwable) {
|
||||
return 'SHA256:'.hash('sha256', $publicKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
app/Actions/Applications/VerifyRepositoryAccess.php
Normal file
51
app/Actions/Applications/VerifyRepositoryAccess.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Applications;
|
||||
|
||||
use App\Models\Application;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
|
||||
class VerifyRepositoryAccess
|
||||
{
|
||||
public function execute(Application $application): bool
|
||||
{
|
||||
if (! $application->deploy_key_private) {
|
||||
throw new RuntimeException('Application does not have a deploy key.');
|
||||
}
|
||||
|
||||
$directory = storage_path('app/private/operations/repository-access-'.$application->id.'-'.str()->random(8));
|
||||
$keyPath = $directory.'/deploy_key';
|
||||
|
||||
File::ensureDirectoryExists($directory, 0700);
|
||||
File::put($keyPath, $application->deploy_key_private);
|
||||
File::chmod($keyPath, 0600);
|
||||
|
||||
try {
|
||||
$result = Process::path($directory)
|
||||
->env([
|
||||
'GIT_SSH_COMMAND' => 'ssh -i '.$keyPath.' -o IdentitiesOnly=yes -o StrictHostKeyChecking=no',
|
||||
])
|
||||
->run([
|
||||
'git',
|
||||
'ls-remote',
|
||||
'--heads',
|
||||
$application->repository_url,
|
||||
$application->default_branch,
|
||||
]);
|
||||
|
||||
if ($result->successful()) {
|
||||
$application->forceFill([
|
||||
'deploy_key_installed_at' => now(),
|
||||
])->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
rescue(fn () => File::deleteDirectory($directory), report: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user