wowowowowo
This commit is contained in:
@@ -8,6 +8,7 @@ use App\Actions\Applications\VerifyRepositoryAccess;
|
||||
use App\Enums\RepositoryType;
|
||||
use App\Enums\ServerStatus;
|
||||
use App\Http\Requests\StoreApplicationRequest;
|
||||
use App\Http\Requests\UpdateApplicationRequest;
|
||||
use App\Models\Application;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -27,9 +28,12 @@ class ApplicationController extends Controller
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
Organisation::findOrFail($request->route('organisation'));
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
|
||||
return inertia('applications/Create');
|
||||
return inertia('applications/Create', [
|
||||
'sourceProviders' => $organisation->sourceProviders()->get(),
|
||||
'repositoryTypes' => RepositoryType::toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreApplicationRequest $request): RedirectResponse
|
||||
@@ -38,8 +42,9 @@ class ApplicationController extends Controller
|
||||
|
||||
$application = $organisation->applications()->create([
|
||||
'name' => $request->string('name')->toString(),
|
||||
'source_provider_id' => $this->sourceProviderIdFor($organisation, $request->integer('source_provider_id') ?: null),
|
||||
'repository_url' => $request->string('repository_url')->toString(),
|
||||
'repository_type' => RepositoryType::GIT,
|
||||
'repository_type' => $request->enum('repository_type', RepositoryType::class),
|
||||
'default_branch' => $request->string('default_branch')->toString(),
|
||||
]);
|
||||
|
||||
@@ -56,14 +61,24 @@ class ApplicationController extends Controller
|
||||
$id = $request->route('application');
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = Application::with([
|
||||
'environments.buildArtifacts' => fn ($query) => $query->latest()->limit(5),
|
||||
'environments.operations' => fn ($query) => $query->latest()->limit(1),
|
||||
'environments.services.replicas',
|
||||
'environments.services.endpoints',
|
||||
'environments.services.slices',
|
||||
'environments.attachments.service',
|
||||
'environments.variables',
|
||||
'organisation',
|
||||
'sourceProvider',
|
||||
])->whereBelongsTo($organisation)->findOrFail($id);
|
||||
|
||||
return inertia('applications/Show', [
|
||||
'application' => $application,
|
||||
'deploymentRequirements' => [
|
||||
'registryRequired' => $organisation->servers()->count() > 1 && $organisation->registries()->doesntExist(),
|
||||
'registryCount' => $organisation->registries()->count(),
|
||||
'serverCount' => $organisation->servers()->count(),
|
||||
],
|
||||
'servers' => inertia()->optional(function () use ($application) {
|
||||
return $application
|
||||
->organisation
|
||||
@@ -75,6 +90,51 @@ class ApplicationController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
|
||||
return inertia('applications/Edit', [
|
||||
'application' => $application,
|
||||
'repositoryTypes' => RepositoryType::toArray(),
|
||||
'sourceProviders' => $organisation->sourceProviders()->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateApplicationRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
|
||||
$application->update([
|
||||
'name' => $request->string('name')->toString(),
|
||||
'source_provider_id' => $this->sourceProviderIdFor($organisation, $request->integer('source_provider_id') ?: null),
|
||||
'repository_type' => $request->enum('repository_type', RepositoryType::class),
|
||||
'repository_url' => $request->string('repository_url')->toString(),
|
||||
'default_branch' => $request->string('default_branch')->toString(),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('applications.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
])
|
||||
->with('success', 'Application updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
|
||||
$application->delete();
|
||||
|
||||
return redirect()
|
||||
->route('applications.index', ['organisation' => $organisation->id])
|
||||
->with('success', 'Application deleted.');
|
||||
}
|
||||
|
||||
public function verifyRepository(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
@@ -86,4 +146,23 @@ class ApplicationController extends Controller
|
||||
|
||||
return back()->with('success', 'Repository access verified.');
|
||||
}
|
||||
|
||||
public function rotateDeployKey(Request $request, GenerateDeployKey $generateDeployKey): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
|
||||
$generateDeployKey->execute($application);
|
||||
|
||||
return back()->with('success', 'Deploy key rotated. Install the new public key before verifying access.');
|
||||
}
|
||||
|
||||
private function sourceProviderIdFor(Organisation $organisation, ?int $sourceProviderId): ?int
|
||||
{
|
||||
if ($sourceProviderId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $organisation->sourceProviders()->findOrFail($sourceProviderId)->id;
|
||||
}
|
||||
}
|
||||
|
||||
44
app/Http/Controllers/BuildArtifactController.php
Normal file
44
app/Http/Controllers/BuildArtifactController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\BuildArtifact;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class BuildArtifactController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
|
||||
return inertia('build-artifacts/Index', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'artifacts' => $environment->buildArtifacts()
|
||||
->with(['builtByOperation', 'builtByService'])
|
||||
->latest()
|
||||
->paginate(30),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
/** @var BuildArtifact $artifact */
|
||||
$artifact = $environment->buildArtifacts()
|
||||
->with(['builtByOperation.steps', 'builtByService'])
|
||||
->findOrFail($request->route('artifact'));
|
||||
|
||||
return inertia('build-artifacts/Show', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'artifact' => $artifact,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Actions\Environments\AttachManagedService;
|
||||
use App\Enums\EnvironmentAttachmentRole;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Http\Requests\StoreEnvironmentAttachmentRequest;
|
||||
use App\Http\Requests\UpdateEnvironmentAttachmentRequest;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -27,6 +28,12 @@ class EnvironmentAttachmentController extends Controller
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'type', 'category']),
|
||||
'roles' => array_values(EnvironmentAttachmentRole::toArray()),
|
||||
'compatibility' => [
|
||||
EnvironmentAttachmentRole::DATABASE->value => [ServiceType::POSTGRES->value],
|
||||
EnvironmentAttachmentRole::CACHE->value => [ServiceType::VALKEY->value],
|
||||
EnvironmentAttachmentRole::QUEUE->value => [ServiceType::VALKEY->value],
|
||||
EnvironmentAttachmentRole::GATEWAY->value => [ServiceType::CADDY->value],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -37,7 +44,7 @@ class EnvironmentAttachmentController extends Controller
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$service = $organisation->services()->findOrFail($request->integer('service_id'));
|
||||
|
||||
app(AttachManagedService::class)->execute(
|
||||
$attachment = app(AttachManagedService::class)->execute(
|
||||
environment: $environment,
|
||||
service: $service,
|
||||
role: $request->enum('role', EnvironmentAttachmentRole::class),
|
||||
@@ -46,6 +53,17 @@ class EnvironmentAttachmentController extends Controller
|
||||
isPrimary: $request->boolean('is_primary', true),
|
||||
);
|
||||
|
||||
if ($request->enum('role', EnvironmentAttachmentRole::class) === EnvironmentAttachmentRole::GATEWAY && $attachment->serviceSlice) {
|
||||
$attachment->serviceSlice->update([
|
||||
'config' => [
|
||||
...($attachment->serviceSlice->config ?? []),
|
||||
'domain' => $request->filled('domain') ? $request->string('domain')->toString() : null,
|
||||
'path_prefix' => $request->filled('path_prefix') ? $request->string('path_prefix')->toString() : '/',
|
||||
'tls_enabled' => $request->boolean('tls_enabled', true),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('environments.show', [
|
||||
'organisation' => $organisation->id,
|
||||
@@ -54,4 +72,73 @@ class EnvironmentAttachmentController extends Controller
|
||||
])
|
||||
->with('success', 'Managed service attached.');
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$attachment = $environment->attachments()
|
||||
->with(['service', 'serviceSlice'])
|
||||
->findOrFail($request->route('attachment'));
|
||||
|
||||
return inertia('environment-attachments/Edit', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'attachment' => $attachment,
|
||||
'roles' => array_values(EnvironmentAttachmentRole::toArray()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateEnvironmentAttachmentRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$attachment = $environment->attachments()->findOrFail($request->route('attachment'));
|
||||
|
||||
$attachment->update([
|
||||
'role' => $request->enum('role', EnvironmentAttachmentRole::class),
|
||||
'env_prefix' => $request->filled('env_prefix') ? $request->string('env_prefix')->toString() : null,
|
||||
'is_primary' => $request->boolean('is_primary'),
|
||||
]);
|
||||
|
||||
if ($attachment->serviceSlice && $request->enum('role', EnvironmentAttachmentRole::class) === EnvironmentAttachmentRole::GATEWAY) {
|
||||
$attachment->serviceSlice->update([
|
||||
'config' => [
|
||||
...($attachment->serviceSlice->config ?? []),
|
||||
'domain' => $request->filled('domain') ? $request->string('domain')->toString() : null,
|
||||
'path_prefix' => $request->filled('path_prefix') ? $request->string('path_prefix')->toString() : '/',
|
||||
'tls_enabled' => $request->boolean('tls_enabled', true),
|
||||
'certificate_status' => $request->filled('certificate_status') ? $request->string('certificate_status')->toString() : null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('environments.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Attachment updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$attachment = $environment->attachments()->findOrFail($request->route('attachment'));
|
||||
|
||||
$attachment->delete();
|
||||
|
||||
return redirect()
|
||||
->route('environments.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Attachment detached.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,54 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Applications\CreateLaravelEnvironment;
|
||||
use App\Enums\BuildStrategy;
|
||||
use App\Enums\EnvironmentAttachmentRole;
|
||||
use App\Enums\SchedulerMode;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Http\Requests\StoreEnvironmentRequest;
|
||||
use App\Http\Requests\UpdateEnvironmentRequest;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Organisation;
|
||||
use App\Support\CaddyRouteRenderer;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Inertia\Response;
|
||||
|
||||
class EnvironmentController extends Controller
|
||||
{
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
|
||||
return inertia('environments/Create', [
|
||||
'application' => $application,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreEnvironmentRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
|
||||
$environment = app(CreateLaravelEnvironment::class)->execute(
|
||||
application: $application,
|
||||
name: $request->string('name')->toString(),
|
||||
branch: $request->string('branch')->toString(),
|
||||
phpVersion: $request->string('php_version')->toString(),
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('environments.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Environment created.');
|
||||
}
|
||||
|
||||
public function show(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
@@ -15,18 +57,151 @@ class EnvironmentController extends Controller
|
||||
$environment = $application->environments()
|
||||
->with([
|
||||
'services.replicas',
|
||||
'services.endpoints',
|
||||
'services.slices',
|
||||
'services.operations.steps',
|
||||
'attachments.service',
|
||||
'attachments.serviceSlice',
|
||||
'variables',
|
||||
'buildArtifacts.builtByService',
|
||||
'operations.steps',
|
||||
'operations.children.target',
|
||||
])
|
||||
->findOrFail($request->route('environment'));
|
||||
|
||||
$serverCount = $this->serverIdsFor($environment)->count();
|
||||
|
||||
return inertia('environments/Show', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'deploymentRequirements' => [
|
||||
'registryRequired' => $organisation->registries()->doesntExist() && $serverCount > 1,
|
||||
'registryCount' => $organisation->registries()->count(),
|
||||
'serverCount' => $serverCount,
|
||||
],
|
||||
'gatewayRoutePreviews' => $this->gatewayRoutePreviews($environment),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()
|
||||
->with('services')
|
||||
->findOrFail($request->route('environment'));
|
||||
|
||||
return inertia('environments/Edit', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'schedulerModes' => array_values(SchedulerMode::toArray()),
|
||||
'buildStrategies' => array_values(BuildStrategy::toArray()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateEnvironmentRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()
|
||||
->with('services')
|
||||
->findOrFail($request->route('environment'));
|
||||
|
||||
$schedulerTargetServiceId = $request->integer('scheduler_target_service_id') ?: null;
|
||||
|
||||
if ($schedulerTargetServiceId !== null) {
|
||||
abort_unless($environment->services()->whereKey($schedulerTargetServiceId)->exists(), 422);
|
||||
}
|
||||
|
||||
$environment->update([
|
||||
'name' => $request->string('name')->toString(),
|
||||
'branch' => $request->string('branch')->toString(),
|
||||
'status' => $request->string('status')->toString(),
|
||||
'scheduler_enabled' => $request->boolean('scheduler_enabled'),
|
||||
'scheduler_target_service_id' => $schedulerTargetServiceId,
|
||||
'scheduler_mode' => $request->enum('scheduler_mode', SchedulerMode::class),
|
||||
'build_config' => [
|
||||
...($environment->build_config ?? []),
|
||||
'build_strategy' => $request->enum('build_strategy', BuildStrategy::class)?->value,
|
||||
'php_version' => $request->string('php_version')->toString(),
|
||||
'document_root' => $request->string('document_root')->toString(),
|
||||
'health_path' => $request->string('health_path')->toString(),
|
||||
'js_package_manager' => $request->string('js_package_manager')->toString(),
|
||||
'js_build_command' => $request->filled('js_build_command')
|
||||
? $request->string('js_build_command')->toString()
|
||||
: null,
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('environments.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Environment updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
|
||||
$environment->delete();
|
||||
|
||||
return redirect()
|
||||
->route('applications.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
])
|
||||
->with('success', 'Environment deleted.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, int>
|
||||
*/
|
||||
private function serverIdsFor(Environment $environment): Collection
|
||||
{
|
||||
return $environment->services
|
||||
->flatMap(fn ($service) => [
|
||||
$service->server_id,
|
||||
...$service->replicas->pluck('server_id')->all(),
|
||||
])
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, array{attachment_id: int, caddyfile: string}>
|
||||
*/
|
||||
private function gatewayRoutePreviews(Environment $environment): Collection
|
||||
{
|
||||
$upstreams = $this->previewGatewayUpstreams($environment);
|
||||
$renderer = app(CaddyRouteRenderer::class);
|
||||
|
||||
return $environment->attachments
|
||||
->filter(fn ($attachment): bool => $attachment->role === EnvironmentAttachmentRole::GATEWAY)
|
||||
->map(fn ($attachment): array => [
|
||||
'attachment_id' => $attachment->id,
|
||||
'caddyfile' => $renderer->render($attachment, $upstreams),
|
||||
])
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function previewGatewayUpstreams(Environment $environment): array
|
||||
{
|
||||
return $environment->services
|
||||
->filter(fn ($service): bool => $service->type === ServiceType::LARAVEL && in_array('web', $service->process_roles ?? [], true))
|
||||
->flatMap(fn ($service) => $service->replicas->map(
|
||||
fn ($replica): string => ($replica->internal_host ?: $replica->container_name).':'.($replica->internal_port ?: 80)
|
||||
))
|
||||
->values()
|
||||
->whenEmpty(fn ($upstreams) => $upstreams->push('web:80'))
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreEnvironmentDeploymentRequest;
|
||||
use App\Jobs\Environments\DeployEnvironment;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class EnvironmentDeploymentController extends Controller
|
||||
{
|
||||
public function store(Organisation $organisation, Application $application, Environment $environment): RedirectResponse
|
||||
public function store(StoreEnvironmentDeploymentRequest $request, Organisation $organisation, Application $application, Environment $environment): RedirectResponse
|
||||
{
|
||||
abort_unless(
|
||||
(int) $application->organisation_id === (int) $organisation->id
|
||||
@@ -18,7 +20,16 @@ class EnvironmentDeploymentController extends Controller
|
||||
404,
|
||||
);
|
||||
|
||||
dispatch(new DeployEnvironment($environment));
|
||||
$environment->loadMissing('services.replicas');
|
||||
|
||||
if ($organisation->registries()->doesntExist() && $this->serverIdsFor($environment)->count() > 1) {
|
||||
return back()->with('error', 'Configure a registry before deploying this environment to multiple servers.');
|
||||
}
|
||||
|
||||
dispatch(new DeployEnvironment(
|
||||
environment: $environment,
|
||||
targetCommit: $request->validated('target_commit') ?: null,
|
||||
));
|
||||
|
||||
return redirect()->route('environments.show', [
|
||||
'organisation' => $organisation->id,
|
||||
@@ -26,4 +37,19 @@ class EnvironmentDeploymentController extends Controller
|
||||
'environment' => $environment->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Support\Collection<int, int>
|
||||
*/
|
||||
private function serverIdsFor(Environment $environment): Collection
|
||||
{
|
||||
return $environment->services
|
||||
->flatMap(fn ($service) => [
|
||||
$service->server_id,
|
||||
...$service->replicas->pluck('server_id')->all(),
|
||||
])
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
}
|
||||
}
|
||||
|
||||
24
app/Http/Controllers/EnvironmentIndexController.php
Normal file
24
app/Http/Controllers/EnvironmentIndexController.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Organisation;
|
||||
use Inertia\Response;
|
||||
|
||||
class EnvironmentIndexController extends Controller
|
||||
{
|
||||
public function __invoke(Organisation $organisation): Response
|
||||
{
|
||||
$applications = $organisation->applications()
|
||||
->with([
|
||||
'environments' => fn ($query) => $query
|
||||
->withCount(['services', 'attachments', 'variables', 'buildArtifacts'])
|
||||
->latest(),
|
||||
])
|
||||
->get();
|
||||
|
||||
return inertia('environments/Index', [
|
||||
'applications' => $applications,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\EnvironmentVariableSource;
|
||||
use App\Http\Requests\ImportEnvironmentVariablesRequest;
|
||||
use App\Http\Requests\StoreEnvironmentVariableRequest;
|
||||
use App\Http\Requests\UpdateEnvironmentVariableRequest;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -11,6 +13,22 @@ use Inertia\Response;
|
||||
|
||||
class EnvironmentVariableController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
|
||||
return inertia('environment-variables/Index', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'variables' => $environment->variables()
|
||||
->with('serviceSlice')
|
||||
->orderBy('key')
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
@@ -35,11 +53,136 @@ class EnvironmentVariableController extends Controller
|
||||
'value' => $request->string('value')->toString(),
|
||||
'source' => EnvironmentVariableSource::USER,
|
||||
'service_slice_id' => null,
|
||||
'overridable' => true,
|
||||
'overridable' => $request->boolean('overridable', true),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('applications.show', ['organisation' => $organisation->id, 'application' => $application->id])
|
||||
->route('environments.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Environment variable saved.');
|
||||
}
|
||||
|
||||
public function import(ImportEnvironmentVariablesRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->parseDotEnv($request->string('contents')->toString()) as $key => $value) {
|
||||
$environment->variables()->updateOrCreate([
|
||||
'key' => $key,
|
||||
], [
|
||||
'value' => $value,
|
||||
'source' => EnvironmentVariableSource::USER,
|
||||
'service_slice_id' => null,
|
||||
'overridable' => $request->boolean('overridable', true),
|
||||
]);
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('environment-variables.index', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', "{$count} environment variables imported.");
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$variable = $environment->variables()->with('serviceSlice')->findOrFail($request->route('variable'));
|
||||
|
||||
return inertia('environment-variables/Edit', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'variable' => $variable,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateEnvironmentVariableRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$variable = $environment->variables()->findOrFail($request->route('variable'));
|
||||
|
||||
$variable->update([
|
||||
'key' => $request->string('key')->toString(),
|
||||
'value' => $request->string('value')->toString(),
|
||||
'overridable' => $request->boolean('overridable'),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('environment-variables.index', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Environment variable updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$variable = $environment->variables()->findOrFail($request->route('variable'));
|
||||
|
||||
$variable->delete();
|
||||
|
||||
return redirect()
|
||||
->route('environment-variables.index', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Environment variable deleted.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function parseDotEnv(string $contents): array
|
||||
{
|
||||
return collect(preg_split('/\R/', $contents) ?: [])
|
||||
->map(fn (string $line): string => trim($line))
|
||||
->reject(fn (string $line): bool => $line === '' || str_starts_with($line, '#'))
|
||||
->mapWithKeys(function (string $line): array {
|
||||
if (str_starts_with($line, 'export ')) {
|
||||
$line = trim(substr($line, 7));
|
||||
}
|
||||
|
||||
[$key, $value] = array_pad(explode('=', $line, 2), 2, '');
|
||||
$key = trim($key);
|
||||
|
||||
if (! preg_match('/^[A-Z][A-Z0-9_]*$/', $key)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$key => $this->unquoteDotEnvValue(trim($value))];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
private function unquoteDotEnvValue(string $value): string
|
||||
{
|
||||
if (str_starts_with($value, '"') && str_ends_with($value, '"')) {
|
||||
return stripcslashes(substr($value, 1, -1));
|
||||
}
|
||||
|
||||
if (str_starts_with($value, "'") && str_ends_with($value, "'")) {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
162
app/Http/Controllers/GatewayRouteController.php
Normal file
162
app/Http/Controllers/GatewayRouteController.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Environments\AttachManagedService;
|
||||
use App\Enums\EnvironmentAttachmentRole;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Http\Requests\StoreGatewayRouteRequest;
|
||||
use App\Http\Requests\UpdateGatewayRouteRequest;
|
||||
use App\Models\EnvironmentAttachment;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class GatewayRouteController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()
|
||||
->with(['attachments.service', 'attachments.serviceSlice'])
|
||||
->findOrFail($request->route('environment'));
|
||||
|
||||
return inertia('gateway-routes/Index', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'routes' => $environment->attachments
|
||||
->filter(fn (EnvironmentAttachment $attachment): bool => $attachment->role === EnvironmentAttachmentRole::GATEWAY)
|
||||
->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
|
||||
return inertia('gateway-routes/Create', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'services' => $organisation->services()
|
||||
->where('type', ServiceType::CADDY->value)
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'type', 'category']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreGatewayRouteRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$service = $organisation->services()
|
||||
->where('type', ServiceType::CADDY->value)
|
||||
->findOrFail($request->integer('service_id'));
|
||||
|
||||
$attachment = app(AttachManagedService::class)->execute(
|
||||
environment: $environment,
|
||||
service: $service,
|
||||
role: EnvironmentAttachmentRole::GATEWAY,
|
||||
name: $request->string('name')->toString(),
|
||||
isPrimary: true,
|
||||
);
|
||||
|
||||
$attachment->serviceSlice?->update([
|
||||
'config' => [
|
||||
...($attachment->serviceSlice->config ?? []),
|
||||
...$this->routeConfig($request),
|
||||
'certificate_status' => $request->boolean('tls_enabled', true) ? 'pending' : 'disabled',
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('gateway.routes.index', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Gateway route created.');
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$route = $environment->attachments()
|
||||
->with(['service', 'serviceSlice'])
|
||||
->where('role', EnvironmentAttachmentRole::GATEWAY->value)
|
||||
->findOrFail($request->route('route'));
|
||||
|
||||
return inertia('gateway-routes/Edit', [
|
||||
'application' => $application,
|
||||
'environment' => $environment,
|
||||
'routeAttachment' => $route,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateGatewayRouteRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$route = $environment->attachments()
|
||||
->with('serviceSlice')
|
||||
->where('role', EnvironmentAttachmentRole::GATEWAY->value)
|
||||
->findOrFail($request->route('route'));
|
||||
|
||||
$route->serviceSlice?->update([
|
||||
'config' => [
|
||||
...($route->serviceSlice->config ?? []),
|
||||
...$this->routeConfig($request),
|
||||
'certificate_status' => $request->filled('certificate_status')
|
||||
? $request->string('certificate_status')->toString()
|
||||
: ($request->boolean('tls_enabled', true) ? 'pending' : 'disabled'),
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('gateway.routes.index', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Gateway route updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$route = $environment->attachments()
|
||||
->where('role', EnvironmentAttachmentRole::GATEWAY->value)
|
||||
->findOrFail($request->route('route'));
|
||||
|
||||
$route->delete();
|
||||
|
||||
return redirect()
|
||||
->route('gateway.routes.index', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $application->id,
|
||||
'environment' => $environment->id,
|
||||
])
|
||||
->with('success', 'Gateway route removed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{domain: string, path_prefix: string, tls_enabled: bool}
|
||||
*/
|
||||
private function routeConfig(StoreGatewayRouteRequest|UpdateGatewayRouteRequest $request): array
|
||||
{
|
||||
return [
|
||||
'domain' => $request->string('domain')->toString(),
|
||||
'path_prefix' => $request->string('path_prefix')->toString(),
|
||||
'tls_enabled' => $request->boolean('tls_enabled', true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@ class OnboardingController extends Controller
|
||||
{
|
||||
$organisation->loadCount(['providers', 'sourceProviders', 'registries', 'servers', 'applications']);
|
||||
|
||||
$applicationNeedingDeployKey = $organisation->applications()
|
||||
->whereNull('deploy_key_installed_at')
|
||||
->first();
|
||||
|
||||
$steps = [
|
||||
[
|
||||
'key' => 'organisation',
|
||||
@@ -48,6 +52,17 @@ class OnboardingController extends Controller
|
||||
'complete' => $organisation->applications_count > 0,
|
||||
'href' => route('applications.create', ['organisation' => $organisation->id]),
|
||||
],
|
||||
[
|
||||
'key' => 'deploy-key',
|
||||
'label' => 'Deploy key',
|
||||
'complete' => $organisation->applications_count === 0 || $applicationNeedingDeployKey === null,
|
||||
'href' => $applicationNeedingDeployKey
|
||||
? route('applications.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'application' => $applicationNeedingDeployKey->id,
|
||||
])
|
||||
: route('applications.index', ['organisation' => $organisation->id]),
|
||||
],
|
||||
];
|
||||
|
||||
$next = collect($steps)->firstWhere('complete', false) ?? $steps[array_key_last($steps)];
|
||||
|
||||
171
app/Http/Controllers/OperationController.php
Normal file
171
app/Http/Controllers/OperationController.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\OperationKind;
|
||||
use App\Enums\OperationStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Operation;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceReplica;
|
||||
use App\Models\ServiceSlice;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class OperationController extends Controller
|
||||
{
|
||||
public function index(Request $request, Organisation $organisation): Response
|
||||
{
|
||||
$applicationIds = $organisation->applications()->pluck('id');
|
||||
$environmentIds = Environment::query()
|
||||
->whereIn('application_id', $applicationIds)
|
||||
->pluck('id');
|
||||
$serverIds = $organisation->servers()->pluck('id');
|
||||
$serviceIds = $organisation->services()->pluck('id');
|
||||
$replicaIds = ServiceReplica::query()
|
||||
->whereIn('service_id', $serviceIds)
|
||||
->pluck('id');
|
||||
$sliceIds = ServiceSlice::query()
|
||||
->whereIn('service_id', $serviceIds)
|
||||
->pluck('id');
|
||||
|
||||
$operations = Operation::query()
|
||||
->with(['target', 'parent', 'children.target'])
|
||||
->withCount('steps', 'children')
|
||||
->where(function (Builder $query) use ($applicationIds, $environmentIds, $serverIds, $serviceIds, $replicaIds, $sliceIds): void {
|
||||
$query
|
||||
->where(function (Builder $query) use ($applicationIds): void {
|
||||
$query->where('target_type', (new Application)->getMorphClass())
|
||||
->whereIn('target_id', $applicationIds);
|
||||
})
|
||||
->orWhere(function (Builder $query) use ($environmentIds): void {
|
||||
$query->where('target_type', (new Environment)->getMorphClass())
|
||||
->whereIn('target_id', $environmentIds);
|
||||
})
|
||||
->orWhere(function (Builder $query) use ($serverIds): void {
|
||||
$query->where('target_type', (new Server)->getMorphClass())
|
||||
->whereIn('target_id', $serverIds);
|
||||
})
|
||||
->orWhere(function (Builder $query) use ($serviceIds): void {
|
||||
$query->where('target_type', (new Service)->getMorphClass())
|
||||
->whereIn('target_id', $serviceIds);
|
||||
})
|
||||
->orWhere(function (Builder $query) use ($replicaIds): void {
|
||||
$query->where('target_type', (new ServiceReplica)->getMorphClass())
|
||||
->whereIn('target_id', $replicaIds);
|
||||
})
|
||||
->orWhere(function (Builder $query) use ($sliceIds): void {
|
||||
$query->where('target_type', (new ServiceSlice)->getMorphClass())
|
||||
->whereIn('target_id', $sliceIds);
|
||||
});
|
||||
})
|
||||
->when($request->filled('kind'), fn (Builder $query) => $query->where('kind', $request->string('kind')->toString()))
|
||||
->when($request->filled('status'), fn (Builder $query) => $query->where('status', $request->string('status')->toString()))
|
||||
->latest()
|
||||
->paginate(30)
|
||||
->withQueryString();
|
||||
|
||||
return inertia('operations/Index', [
|
||||
'operations' => $operations,
|
||||
'filters' => $request->only(['kind', 'status']),
|
||||
'operationKinds' => OperationKind::toArray(),
|
||||
'operationStatuses' => OperationStatus::toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Organisation $organisation, Operation $operation): Response
|
||||
{
|
||||
abort_unless($this->operationBelongsToOrganisation($operation, $organisation), 404);
|
||||
|
||||
$operation->load([
|
||||
'target',
|
||||
'parent.target',
|
||||
'children.target',
|
||||
'children.steps',
|
||||
'steps',
|
||||
]);
|
||||
|
||||
return inertia('operations/Show', [
|
||||
'operation' => $operation,
|
||||
]);
|
||||
}
|
||||
|
||||
public function retry(Organisation $organisation, Operation $operation): RedirectResponse
|
||||
{
|
||||
abort_unless($this->operationBelongsToOrganisation($operation, $organisation), 404);
|
||||
|
||||
$operation->update([
|
||||
'status' => OperationStatus::PENDING,
|
||||
'started_at' => null,
|
||||
'finished_at' => null,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('operations.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'operation' => $operation->id,
|
||||
])
|
||||
->with('success', 'Operation queued to run again.');
|
||||
}
|
||||
|
||||
public function cancel(Organisation $organisation, Operation $operation): RedirectResponse
|
||||
{
|
||||
abort_unless($this->operationBelongsToOrganisation($operation, $organisation), 404);
|
||||
|
||||
$operation->update([
|
||||
'status' => OperationStatus::CANCELLED,
|
||||
'finished_at' => now(),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('operations.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'operation' => $operation->id,
|
||||
])
|
||||
->with('success', 'Operation cancelled.');
|
||||
}
|
||||
|
||||
public function downloadLogs(Organisation $organisation, Operation $operation): StreamedResponse
|
||||
{
|
||||
abort_unless($this->operationBelongsToOrganisation($operation, $organisation), 404);
|
||||
|
||||
$operation->load('steps');
|
||||
|
||||
return response()->streamDownload(function () use ($operation): void {
|
||||
foreach ($operation->steps as $step) {
|
||||
echo "# {$step->name}\n\n";
|
||||
|
||||
if ($step->logs) {
|
||||
echo "## Logs\n{$step->logs}\n\n";
|
||||
}
|
||||
|
||||
if ($step->error_logs) {
|
||||
echo "## Error Logs\n{$step->error_logs}\n\n";
|
||||
}
|
||||
}
|
||||
}, "operation-{$operation->hash}.log", [
|
||||
'Content-Type' => 'text/plain',
|
||||
]);
|
||||
}
|
||||
|
||||
private function operationBelongsToOrganisation(Operation $operation, Organisation $organisation): bool
|
||||
{
|
||||
$target = $operation->target;
|
||||
|
||||
return match (true) {
|
||||
$target instanceof Application => $target->organisation_id === $organisation->id,
|
||||
$target instanceof Environment => $target->application()->where('organisation_id', $organisation->id)->exists(),
|
||||
$target instanceof Server => $target->organisation_id === $organisation->id,
|
||||
$target instanceof Service => $target->organisation_id === $organisation->id,
|
||||
$target instanceof ServiceReplica => $target->service()->where('organisation_id', $organisation->id)->exists(),
|
||||
$target instanceof ServiceSlice => $target->service()->where('organisation_id', $organisation->id)->exists(),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\ServiceStatus;
|
||||
use App\Models\Operation;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Provider;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -15,7 +18,23 @@ class OrganisationController extends Controller
|
||||
'providers' => Inertia::lazy(fn () => Provider::whereOrganisationId($request->route('organisation'))->get()),
|
||||
'registries' => Inertia::lazy(fn () => Organisation::findOrFail($request->route('organisation'))->registries()->get()),
|
||||
'sourceProviders' => Inertia::lazy(fn () => Organisation::findOrFail($request->route('organisation'))->sourceProviders()->get()),
|
||||
'organisation' => Organisation::withCount('servers', 'applications', 'members')->findOrFail($request->route('organisation')),
|
||||
'organisation' => Organisation::with('members')
|
||||
->withCount('servers', 'applications', 'members', 'providers', 'sourceProviders', 'registries')
|
||||
->findOrFail($request->route('organisation')),
|
||||
'health' => [
|
||||
'unhealthy_services' => Service::query()
|
||||
->where('organisation_id', $request->route('organisation'))
|
||||
->whereNot('status', ServiceStatus::RUNNING)
|
||||
->count(),
|
||||
'failed_operations' => Operation::query()
|
||||
->whereHasMorph('target', [Service::class], fn ($query) => $query->where('organisation_id', $request->route('organisation')))
|
||||
->where('status', 'failed')
|
||||
->count(),
|
||||
'locked_variables' => Organisation::findOrFail($request->route('organisation'))
|
||||
->applications()
|
||||
->whereHas('environments.variables', fn ($query) => $query->where('overridable', false))
|
||||
->count(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
120
app/Http/Controllers/OrganisationMemberController.php
Normal file
120
app/Http/Controllers/OrganisationMemberController.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\OrganisationRole;
|
||||
use App\Http\Requests\StoreOrganisationMemberRequest;
|
||||
use App\Http\Requests\UpdateOrganisationInvitationRequest;
|
||||
use App\Http\Requests\UpdateOrganisationMemberRequest;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\OrganisationInvitation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Response;
|
||||
|
||||
class OrganisationMemberController extends Controller
|
||||
{
|
||||
public function index(Organisation $organisation): Response
|
||||
{
|
||||
return inertia('organisation-members/Index', [
|
||||
'organisation' => $organisation->load(['members', 'invitations.invitedBy']),
|
||||
'roles' => array_values(OrganisationRole::toArray()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreOrganisationMemberRequest $request, Organisation $organisation): RedirectResponse
|
||||
{
|
||||
$email = Str::lower($request->string('email')->toString());
|
||||
$user = User::query()
|
||||
->where('email', $email)
|
||||
->first();
|
||||
|
||||
if ($user === null) {
|
||||
abort_if(
|
||||
$organisation->invitations()->where('email', $email)->whereNull('accepted_at')->exists(),
|
||||
422,
|
||||
'This email already has a pending invitation.'
|
||||
);
|
||||
|
||||
$organisation->invitations()->create([
|
||||
'email' => $email,
|
||||
'role' => $request->enum('role', OrganisationRole::class),
|
||||
'token' => Str::random(40),
|
||||
'invited_by_user_id' => $request->user()?->id,
|
||||
'expires_at' => now()->addDays(14),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('organisation-members.index', ['organisation' => $organisation->id])
|
||||
->with('success', 'Invitation created.');
|
||||
}
|
||||
|
||||
$organisation->members()->syncWithoutDetaching([
|
||||
$user->id => ['role' => $request->enum('role', OrganisationRole::class)],
|
||||
]);
|
||||
|
||||
$organisation->invitations()
|
||||
->where('email', $email)
|
||||
->delete();
|
||||
|
||||
return redirect()
|
||||
->route('organisation-members.index', ['organisation' => $organisation->id])
|
||||
->with('success', 'Member added.');
|
||||
}
|
||||
|
||||
public function update(UpdateOrganisationMemberRequest $request, Organisation $organisation, User $member): RedirectResponse
|
||||
{
|
||||
abort_unless($organisation->members()->whereKey($member->id)->exists(), 404);
|
||||
|
||||
$organisation->members()->updateExistingPivot($member->id, [
|
||||
'role' => $request->enum('role', OrganisationRole::class),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('organisation-members.index', ['organisation' => $organisation->id])
|
||||
->with('success', 'Member role updated.');
|
||||
}
|
||||
|
||||
public function updateInvitation(
|
||||
UpdateOrganisationInvitationRequest $request,
|
||||
Organisation $organisation,
|
||||
OrganisationInvitation $invitation
|
||||
): RedirectResponse {
|
||||
abort_unless($invitation->organisation_id === $organisation->id, 404);
|
||||
|
||||
$invitation->update([
|
||||
'role' => $request->enum('role', OrganisationRole::class),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('organisation-members.index', ['organisation' => $organisation->id])
|
||||
->with('success', 'Invitation role updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Organisation $organisation, User $member): RedirectResponse
|
||||
{
|
||||
abort_if($organisation->owner_id === $member->id, 422, 'The organisation owner cannot be removed.');
|
||||
|
||||
$organisation->members()->detach($member->id);
|
||||
|
||||
return redirect()
|
||||
->route('organisation-members.index', ['organisation' => $organisation->id])
|
||||
->with('success', 'Member removed.');
|
||||
}
|
||||
|
||||
public function destroyInvitation(
|
||||
Request $request,
|
||||
Organisation $organisation,
|
||||
OrganisationInvitation $invitation
|
||||
): RedirectResponse {
|
||||
abort_unless($invitation->organisation_id === $organisation->id, 404);
|
||||
|
||||
$invitation->delete();
|
||||
|
||||
return redirect()
|
||||
->route('organisation-members.index', ['organisation' => $organisation->id])
|
||||
->with('success', 'Invitation cancelled.');
|
||||
}
|
||||
}
|
||||
49
app/Http/Controllers/ProviderController.php
Normal file
49
app/Http/Controllers/ProviderController.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\ProviderType;
|
||||
use App\Http\Requests\StoreProviderRequest;
|
||||
use App\Models\Organisation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProviderController extends Controller
|
||||
{
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
Organisation::findOrFail($request->route('organisation'));
|
||||
|
||||
return inertia('providers/Create', [
|
||||
'providerTypes' => array_values(ProviderType::toArray()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreProviderRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
|
||||
$organisation->providers()->create([
|
||||
'name' => $request->string('name')->toString(),
|
||||
'type' => $request->enum('type', ProviderType::class),
|
||||
'token' => $request->string('token')->toString(),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('organisations.show', ['organisation' => $organisation->id])
|
||||
->with('success', 'Server provider created.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$provider = $organisation->providers()->findOrFail($request->route('provider'));
|
||||
|
||||
$provider->delete();
|
||||
|
||||
return redirect()
|
||||
->route('organisations.show', ['organisation' => $organisation->id])
|
||||
->with('success', 'Server provider deleted.');
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,27 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\RegistryType;
|
||||
use App\Http\Requests\StoreRegistryRequest;
|
||||
use App\Http\Requests\UpdateRegistryRequest;
|
||||
use App\Models\BuildArtifact;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Registry;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class RegistryController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
|
||||
return inertia('registries/Index', [
|
||||
'registries' => $organisation->registries()
|
||||
->latest()
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
Organisation::findOrFail($request->route('organisation'));
|
||||
@@ -38,4 +52,75 @@ class RegistryController extends Controller
|
||||
->route('organisations.show', ['organisation' => $organisation->id])
|
||||
->with('success', 'Registry created.');
|
||||
}
|
||||
|
||||
public function show(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
/** @var Registry $registry */
|
||||
$registry = $organisation->registries()->findOrFail($request->route('registry'));
|
||||
$registryUrl = rtrim((string) $registry->url, '/');
|
||||
$artifacts = BuildArtifact::query()
|
||||
->with(['environment.application', 'builtByService'])
|
||||
->whereHas('environment.application', fn ($query) => $query->where('organisation_id', $organisation->id))
|
||||
->when($registryUrl !== '', fn ($query) => $query->where('registry_ref', 'like', $registryUrl.'%'));
|
||||
|
||||
return inertia('registries/Show', [
|
||||
'registry' => $registry,
|
||||
'artifactCount' => (clone $artifacts)->count(),
|
||||
'environmentCount' => (clone $artifacts)->distinct('environment_id')->count('environment_id'),
|
||||
'artifacts' => $artifacts
|
||||
->latest()
|
||||
->paginate(20),
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$registry = $organisation->registries()->findOrFail($request->route('registry'));
|
||||
|
||||
return inertia('registries/Edit', [
|
||||
'registry' => $registry,
|
||||
'registryTypes' => array_values(RegistryType::toArray()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateRegistryRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
/** @var Registry $registry */
|
||||
$registry = $organisation->registries()->findOrFail($request->route('registry'));
|
||||
|
||||
$credentials = $registry->credentials ?? [];
|
||||
$username = $request->string('username')->toString();
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$credentials['password'] = $request->string('password')->toString();
|
||||
}
|
||||
|
||||
$credentials['username'] = $username;
|
||||
|
||||
$registry->update([
|
||||
'name' => $request->string('name')->toString(),
|
||||
'type' => $request->enum('type', RegistryType::class),
|
||||
'url' => rtrim($request->string('url')->toString(), '/'),
|
||||
'credentials' => $credentials,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('organisations.show', ['organisation' => $organisation->id])
|
||||
->with('success', 'Registry updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$registry = $organisation->registries()->findOrFail($request->route('registry'));
|
||||
|
||||
$registry->delete();
|
||||
|
||||
return redirect()
|
||||
->route('organisations.show', ['organisation' => $organisation->id])
|
||||
->with('success', 'Registry deleted.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,33 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\GenerateRandomSlug;
|
||||
use App\Enums\OperationKind;
|
||||
use App\Enums\OperationStatus;
|
||||
use App\Enums\ServerStatus;
|
||||
use App\Jobs\Servers\WaitForServerToConnect;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Provider;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Response;
|
||||
|
||||
class ServerController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
|
||||
return inertia('servers/Index', [
|
||||
'servers' => $organisation->servers()->paginate(30),
|
||||
'networks' => $organisation->networks()
|
||||
->with(['servers' => fn ($query) => $query->select('id', 'network_id', 'name', 'private_ip', 'status')])
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
|
||||
@@ -55,7 +62,7 @@ class ServerController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'provider' => ['required', 'exists:providers,id'],
|
||||
@@ -135,13 +142,63 @@ class ServerController extends Controller
|
||||
return redirect()->route('servers.show', ['organisation' => $organisation->id, 'server' => $server->id]);
|
||||
}
|
||||
|
||||
public function show(Request $request)
|
||||
public function show(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
|
||||
return inertia('servers/Show', [
|
||||
'server' => $server->load('services.slices', 'serviceOperations.steps', 'serviceOperations.target'),
|
||||
'server' => $server->load(
|
||||
'firewallRules',
|
||||
'network',
|
||||
'operations.steps',
|
||||
'operations.children.target',
|
||||
'services.slices',
|
||||
'services.endpoints',
|
||||
'serviceOperations.steps',
|
||||
'serviceOperations.children.target',
|
||||
'serviceOperations.target',
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
|
||||
$server->delete();
|
||||
|
||||
return redirect()
|
||||
->route('servers.index', ['organisation' => $organisation->id])
|
||||
->with('success', 'Server deleted.');
|
||||
}
|
||||
|
||||
public function heal(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
|
||||
$operation = $server->operations()->create([
|
||||
'kind' => OperationKind::SERVER_PROVISION,
|
||||
'status' => OperationStatus::PENDING,
|
||||
]);
|
||||
|
||||
foreach ([
|
||||
'Check server shell' => 'true',
|
||||
'Check Docker' => 'docker --version && docker compose version',
|
||||
'Check Keystone directories' => 'test -d /home/keystone && test -d /home/keystone/services',
|
||||
] as $order => $script) {
|
||||
$operation->steps()->create([
|
||||
'name' => $order,
|
||||
'order' => $operation->steps()->count() + 1,
|
||||
'status' => OperationStatus::PENDING,
|
||||
'script' => $script,
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('servers.show', ['organisation' => $organisation->id, 'server' => $server->id])
|
||||
->with('success', 'Server heal operation queued.');
|
||||
}
|
||||
}
|
||||
|
||||
41
app/Http/Controllers/ServerFirewallRuleController.php
Normal file
41
app/Http/Controllers/ServerFirewallRuleController.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\FirewallRules\UninstallFirewallRule;
|
||||
use App\Http\Requests\StoreServerFirewallRuleRequest;
|
||||
use App\Models\FirewallRule;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ServerFirewallRuleController extends Controller
|
||||
{
|
||||
public function store(StoreServerFirewallRuleRequest $request, Organisation $organisation, Server $server): RedirectResponse
|
||||
{
|
||||
abort_unless((int) $server->organisation_id === (int) $organisation->id, 404);
|
||||
|
||||
$server->firewallRules()->create($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('servers.show', [$organisation, $server])
|
||||
->with('success', 'Firewall rule queued for installation.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Organisation $organisation, Server $server, FirewallRule $firewallRule, UninstallFirewallRule $uninstallFirewallRule): RedirectResponse
|
||||
{
|
||||
abort_unless(
|
||||
(int) $server->organisation_id === (int) $organisation->id
|
||||
&& (int) $firewallRule->server_id === (int) $server->id,
|
||||
404,
|
||||
);
|
||||
|
||||
$uninstallFirewallRule->execute($firewallRule);
|
||||
$firewallRule->delete();
|
||||
|
||||
return redirect()
|
||||
->route('servers.show', [$organisation, $server])
|
||||
->with('success', 'Firewall rule removed.');
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Services\CreateService;
|
||||
use App\Enums\DeployPolicy;
|
||||
use App\Enums\ServiceCategory;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Http\Requests\StoreServiceRequest;
|
||||
use App\Http\Requests\UpdateServiceRequest;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -49,7 +51,7 @@ class ServiceController extends Controller
|
||||
{
|
||||
$server = Server::findOrFail($request->route('server'));
|
||||
$service = $server->services()
|
||||
->with(['replicas', 'slices', 'operations.steps', 'environment.application'])
|
||||
->with(['replicas', 'slices', 'endpoints', 'operations.steps', 'operations.children.target', 'environment.application'])
|
||||
->findOrFail($request->route('service'));
|
||||
|
||||
return inertia('services/Show', [
|
||||
@@ -58,6 +60,23 @@ class ServiceController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function showForEnvironment(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$application = $organisation->applications()->findOrFail($request->route('application'));
|
||||
$environment = $application->environments()->findOrFail($request->route('environment'));
|
||||
$service = $environment->services()
|
||||
->with(['server', 'replicas', 'slices', 'endpoints', 'operations.steps', 'operations.children.target', 'environment.application'])
|
||||
->findOrFail($request->route('service'));
|
||||
|
||||
return inertia('services/Show', [
|
||||
'server' => $service->server,
|
||||
'service' => $service,
|
||||
'environment' => $environment,
|
||||
'application' => $application,
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$server = Server::findOrFail($request->route('server'));
|
||||
@@ -66,6 +85,7 @@ class ServiceController extends Controller
|
||||
return inertia('services/Edit', [
|
||||
'server' => $server,
|
||||
'service' => $service,
|
||||
'deployPolicies' => array_values(DeployPolicy::toArray()),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -74,7 +94,31 @@ class ServiceController extends Controller
|
||||
$server = Server::findOrFail($request->route('server'));
|
||||
$service = $server->services()->findOrFail($request->route('service'));
|
||||
|
||||
$service->update($request->validated());
|
||||
$validated = $request->validated();
|
||||
|
||||
$service->update([
|
||||
'name' => $validated['name'],
|
||||
'desired_replicas' => $validated['desired_replicas'],
|
||||
'default_cpu_limit' => $validated['default_cpu_limit'] ?? null,
|
||||
'default_memory_limit_mb' => $validated['default_memory_limit_mb'] ?? null,
|
||||
'deploy_policy' => $request->enum('deploy_policy', DeployPolicy::class) ?? $service->deploy_policy,
|
||||
'version_track' => $validated['version_track'] ?? $service->version_track,
|
||||
'available_image_digest' => $validated['available_image_digest'] ?? null,
|
||||
'process_roles' => collect(explode(',', $validated['process_roles'] ?? ''))
|
||||
->map(fn (string $role): string => trim($role))
|
||||
->filter()
|
||||
->values()
|
||||
->all(),
|
||||
'config' => [
|
||||
...($service->config ?? []),
|
||||
'migration_mode' => $validated['migration_mode'] ?? null,
|
||||
'migration_timing' => $validated['migration_timing'] ?? null,
|
||||
'migration_command' => $validated['migration_command'] ?? null,
|
||||
'health_path' => $validated['health_path'] ?? null,
|
||||
'backup_enabled' => $request->boolean('backup_enabled'),
|
||||
'backup_command' => $validated['backup_command'] ?? null,
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('services.show', [
|
||||
@@ -84,4 +128,19 @@ class ServiceController extends Controller
|
||||
])
|
||||
->with('success', 'Service updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$server = Server::findOrFail($request->route('server'));
|
||||
$service = $server->services()->findOrFail($request->route('service'));
|
||||
|
||||
$service->delete();
|
||||
|
||||
return redirect()
|
||||
->route('servers.show', [
|
||||
'organisation' => $server->organisation_id,
|
||||
'server' => $server->id,
|
||||
])
|
||||
->with('success', 'Service deleted.');
|
||||
}
|
||||
}
|
||||
|
||||
102
app/Http/Controllers/ServiceReplicaController.php
Normal file
102
app/Http/Controllers/ServiceReplicaController.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\OperationKind;
|
||||
use App\Enums\OperationStatus;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\ServiceReplica;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class ServiceReplicaController extends Controller
|
||||
{
|
||||
public function show(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
$service = $server->services()->with('environment.application')->findOrFail($request->route('service'));
|
||||
$replica = $service->replicas()
|
||||
->with(['server', 'operation.steps', 'operations.steps', 'operations.children.target'])
|
||||
->findOrFail($request->route('replica'));
|
||||
|
||||
return inertia('service-replicas/Show', [
|
||||
'server' => $server,
|
||||
'service' => $service,
|
||||
'replica' => $replica,
|
||||
]);
|
||||
}
|
||||
|
||||
public function restart(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
$service = $server->services()->findOrFail($request->route('service'));
|
||||
$replica = $service->replicas()->findOrFail($request->route('replica'));
|
||||
|
||||
$this->queueLifecycleOperation($replica, 'Restart replica', "docker restart {$replica->container_name}");
|
||||
|
||||
return redirect()
|
||||
->route('service-replicas.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'server' => $server->id,
|
||||
'service' => $service->id,
|
||||
'replica' => $replica->id,
|
||||
])
|
||||
->with('success', 'Replica restart queued.');
|
||||
}
|
||||
|
||||
public function start(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
$service = $server->services()->findOrFail($request->route('service'));
|
||||
$replica = $service->replicas()->findOrFail($request->route('replica'));
|
||||
|
||||
$this->queueLifecycleOperation($replica, 'Start replica', "docker start {$replica->container_name}");
|
||||
|
||||
return redirect()
|
||||
->route('service-replicas.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'server' => $server->id,
|
||||
'service' => $service->id,
|
||||
'replica' => $replica->id,
|
||||
])
|
||||
->with('success', 'Replica start queued.');
|
||||
}
|
||||
|
||||
public function stop(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
$service = $server->services()->findOrFail($request->route('service'));
|
||||
$replica = $service->replicas()->findOrFail($request->route('replica'));
|
||||
|
||||
$this->queueLifecycleOperation($replica, 'Stop replica', "docker stop {$replica->container_name}");
|
||||
|
||||
return redirect()
|
||||
->route('service-replicas.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'server' => $server->id,
|
||||
'service' => $service->id,
|
||||
'replica' => $replica->id,
|
||||
])
|
||||
->with('success', 'Replica stop queued.');
|
||||
}
|
||||
|
||||
private function queueLifecycleOperation(ServiceReplica $replica, string $name, string $script): void
|
||||
{
|
||||
$operation = $replica->operations()->create([
|
||||
'kind' => OperationKind::REPLICA_DEPLOY,
|
||||
'status' => OperationStatus::PENDING,
|
||||
]);
|
||||
|
||||
$operation->steps()->create([
|
||||
'name' => $name,
|
||||
'order' => 1,
|
||||
'status' => OperationStatus::PENDING,
|
||||
'script' => $script,
|
||||
]);
|
||||
}
|
||||
}
|
||||
104
app/Http/Controllers/ServiceSliceController.php
Normal file
104
app/Http/Controllers/ServiceSliceController.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreServiceSliceRequest;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\ServiceSlice;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class ServiceSliceController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
$service = $server->services()
|
||||
->with('environment.application')
|
||||
->findOrFail($request->route('service'));
|
||||
|
||||
return inertia('service-slices/Index', [
|
||||
'server' => $server,
|
||||
'service' => $service,
|
||||
'slices' => $service->slices()
|
||||
->with(['environment.application', 'attachments', 'operations.steps', 'operations.children.target'])
|
||||
->latest()
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
$service = $server->services()->with('environment.application')->findOrFail($request->route('service'));
|
||||
$slice = $service->slices()
|
||||
->with(['environment.application', 'attachments.environment.application', 'operations.steps', 'operations.children.target'])
|
||||
->findOrFail($request->route('slice'));
|
||||
|
||||
return inertia('service-slices/Show', [
|
||||
'server' => $server,
|
||||
'service' => $service,
|
||||
'slice' => $slice,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
$service = $server->services()->findOrFail($request->route('service'));
|
||||
|
||||
return inertia('service-slices/Create', [
|
||||
'server' => $server,
|
||||
'service' => $service,
|
||||
'environments' => $organisation->applications()
|
||||
->with('environments')
|
||||
->get()
|
||||
->pluck('environments')
|
||||
->flatten()
|
||||
->values(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreServiceSliceRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$server = $organisation->servers()->findOrFail($request->route('server'));
|
||||
$service = $server->services()->findOrFail($request->route('service'));
|
||||
$environmentId = $request->integer('environment_id') ?: null;
|
||||
|
||||
if ($environmentId !== null) {
|
||||
$belongsToOrganisation = $organisation->applications()
|
||||
->whereHas('environments', fn ($query) => $query->whereKey($environmentId))
|
||||
->exists();
|
||||
|
||||
abort_unless($belongsToOrganisation, 422);
|
||||
}
|
||||
|
||||
$config = $request->filled('config')
|
||||
? json_decode($request->string('config')->toString(), true, flags: JSON_THROW_ON_ERROR)
|
||||
: [];
|
||||
|
||||
/** @var ServiceSlice $slice */
|
||||
$slice = $service->slices()->create([
|
||||
'environment_id' => $environmentId,
|
||||
'name' => $request->string('name')->toString(),
|
||||
'type' => $request->string('type')->toString(),
|
||||
'status' => $request->string('status')->toString(),
|
||||
'config' => $config,
|
||||
'credentials' => [],
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('service-slices.show', [
|
||||
'organisation' => $organisation->id,
|
||||
'server' => $server->id,
|
||||
'service' => $service->id,
|
||||
'slice' => $slice->id,
|
||||
])
|
||||
->with('success', 'Slice created.');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Services\CreateStatefulServiceUpdateOperation;
|
||||
use App\Actions\Services\ResolveServiceImageDigest;
|
||||
use App\Enums\ServiceType;
|
||||
use App\Http\Requests\StoreServiceUpdateRequest;
|
||||
use App\Models\Organisation;
|
||||
@@ -33,6 +34,7 @@ class ServiceUpdateController extends Controller
|
||||
CreateStatefulServiceUpdateOperation $createStatefulServiceUpdateOperation,
|
||||
): RedirectResponse {
|
||||
abort_unless((int) $server->organisation_id === (int) $organisation->id && (int) $service->server_id === (int) $server->id, 404);
|
||||
abort_unless($request->string('confirmation')->toString() === $service->name, 422);
|
||||
|
||||
$createStatefulServiceUpdateOperation->execute(
|
||||
service: $service,
|
||||
@@ -45,4 +47,26 @@ class ServiceUpdateController extends Controller
|
||||
'server' => $server->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function resolve(
|
||||
Organisation $organisation,
|
||||
Server $server,
|
||||
Service $service,
|
||||
ResolveServiceImageDigest $resolveServiceImageDigest,
|
||||
): RedirectResponse {
|
||||
abort_unless((int) $server->organisation_id === (int) $organisation->id && (int) $service->server_id === (int) $server->id, 404);
|
||||
abort_unless(in_array($service->type, [ServiceType::POSTGRES, ServiceType::VALKEY], true), 404);
|
||||
|
||||
$service->update([
|
||||
'available_image_digest' => $resolveServiceImageDigest->execute($service),
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('service-updates.create', [
|
||||
'organisation' => $organisation->id,
|
||||
'server' => $server->id,
|
||||
'service' => $service->id,
|
||||
])
|
||||
->with('success', 'Latest image digest resolved.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,26 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\SourceProviderType;
|
||||
use App\Http\Requests\StoreSourceProviderRequest;
|
||||
use App\Http\Requests\UpdateSourceProviderRequest;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\SourceProvider;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class SourceProviderController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
|
||||
return inertia('source-providers/Index', [
|
||||
'sourceProviders' => $organisation->sourceProviders()
|
||||
->latest()
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
Organisation::findOrFail($request->route('organisation'));
|
||||
@@ -35,4 +48,44 @@ class SourceProviderController extends Controller
|
||||
->route('organisations.show', ['organisation' => $organisation->id])
|
||||
->with('success', 'Source provider created.');
|
||||
}
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$sourceProvider = $organisation->sourceProviders()->findOrFail($request->route('source_provider'));
|
||||
|
||||
return inertia('source-providers/Edit', [
|
||||
'sourceProvider' => $sourceProvider,
|
||||
'sourceProviderTypes' => array_values(SourceProviderType::toArray()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateSourceProviderRequest $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
/** @var SourceProvider $sourceProvider */
|
||||
$sourceProvider = $organisation->sourceProviders()->findOrFail($request->route('source_provider'));
|
||||
|
||||
$sourceProvider->update([
|
||||
'name' => $request->string('name')->toString(),
|
||||
'type' => $request->enum('type', SourceProviderType::class),
|
||||
'url' => $request->filled('url') ? rtrim($request->string('url')->toString(), '/') : null,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('organisations.show', ['organisation' => $organisation->id])
|
||||
->with('success', 'Source provider updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$organisation = Organisation::findOrFail($request->route('organisation'));
|
||||
$sourceProvider = $organisation->sourceProviders()->findOrFail($request->route('source_provider'));
|
||||
|
||||
$sourceProvider->delete();
|
||||
|
||||
return redirect()
|
||||
->route('organisations.show', ['organisation' => $organisation->id])
|
||||
->with('success', 'Source provider deleted.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ class HandleInertiaRequests extends Middleware
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'organisation' => $request->route('organisation')
|
||||
? Organisation::with('applications')->findOrFail($this->routeKey($request->route('organisation')))
|
||||
? Organisation::with('applications.environments')
|
||||
->withCount(['providers', 'sourceProviders', 'registries', 'servers', 'applications'])
|
||||
->findOrFail($this->routeKey($request->route('organisation')))
|
||||
: null,
|
||||
'application' => $request->route('application')
|
||||
? Application::with('environments')->findOrFail($this->routeKey($request->route('application')))
|
||||
|
||||
29
app/Http/Requests/ImportEnvironmentVariablesRequest.php
Normal file
29
app/Http/Requests/ImportEnvironmentVariablesRequest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ImportEnvironmentVariablesRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'contents' => ['required', 'string', 'max:20000'],
|
||||
'overridable' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\RepositoryType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreApplicationRequest extends FormRequest
|
||||
{
|
||||
@@ -23,6 +25,8 @@ class StoreApplicationRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'source_provider_id' => ['nullable', 'integer', 'exists:source_providers,id'],
|
||||
'repository_type' => ['required', Rule::enum(RepositoryType::class)],
|
||||
'repository_url' => ['required', 'string', 'max:255', 'regex:/^(git@[^:]+:.+|ssh:\/\/.+)$/i'],
|
||||
'default_branch' => ['required', 'string', 'max:255', 'regex:/^[A-Za-z0-9._\/-]+$/'],
|
||||
'environment_name' => ['required', 'string', 'max:255'],
|
||||
|
||||
@@ -29,6 +29,9 @@ class StoreEnvironmentAttachmentRequest extends FormRequest
|
||||
'name' => ['nullable', 'string', 'max:255'],
|
||||
'env_prefix' => ['nullable', 'string', 'max:32', 'regex:/^[A-Z][A-Z0-9_]*$/'],
|
||||
'is_primary' => ['boolean'],
|
||||
'domain' => ['nullable', 'string', 'max:255'],
|
||||
'path_prefix' => ['nullable', 'string', 'max:255'],
|
||||
'tls_enabled' => ['boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
28
app/Http/Requests/StoreEnvironmentDeploymentRequest.php
Normal file
28
app/Http/Requests/StoreEnvironmentDeploymentRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreEnvironmentDeploymentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'target_commit' => ['nullable', 'string', 'size:40', 'regex:/^[a-fA-F0-9]{40}$/'],
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/StoreEnvironmentRequest.php
Normal file
30
app/Http/Requests/StoreEnvironmentRequest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreEnvironmentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'branch' => ['required', 'string', 'max:255', 'regex:/^[A-Za-z0-9._\/-]+$/'],
|
||||
'php_version' => ['required', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ class StoreEnvironmentVariableRequest extends FormRequest
|
||||
return [
|
||||
'key' => ['required', 'string', 'max:255', 'regex:/^[A-Z][A-Z0-9_]*$/'],
|
||||
'value' => ['nullable', 'string'],
|
||||
'overridable' => ['boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
32
app/Http/Requests/StoreGatewayRouteRequest.php
Normal file
32
app/Http/Requests/StoreGatewayRouteRequest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGatewayRouteRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'service_id' => ['required', 'integer', 'exists:services,id'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'domain' => ['required', 'string', 'max:255'],
|
||||
'path_prefix' => ['required', 'string', 'max:255', 'regex:/^\//'],
|
||||
'tls_enabled' => ['boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/StoreOrganisationMemberRequest.php
Normal file
31
app/Http/Requests/StoreOrganisationMemberRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\OrganisationRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreOrganisationMemberRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'role' => ['required', Rule::enum(OrganisationRole::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
32
app/Http/Requests/StoreProviderRequest.php
Normal file
32
app/Http/Requests/StoreProviderRequest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\ProviderType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreProviderRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', Rule::enum(ProviderType::class)],
|
||||
'token' => ['required', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
32
app/Http/Requests/StoreServerFirewallRuleRequest.php
Normal file
32
app/Http/Requests/StoreServerFirewallRuleRequest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\FirewallRuleType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreServerFirewallRuleRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['required', Rule::enum(FirewallRuleType::class)],
|
||||
'ports' => ['required', 'string', 'max:50', 'regex:/^[A-Za-z0-9:\/,-]+$/'],
|
||||
'from' => ['nullable', 'string', 'max:255', 'regex:/^[A-Fa-f0-9:.\/-]+$/'],
|
||||
];
|
||||
}
|
||||
}
|
||||
32
app/Http/Requests/StoreServiceSliceRequest.php
Normal file
32
app/Http/Requests/StoreServiceSliceRequest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreServiceSliceRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', 'string', 'max:255'],
|
||||
'environment_id' => ['nullable', 'integer'],
|
||||
'status' => ['required', 'string', 'max:255'],
|
||||
'config' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ class StoreServiceUpdateRequest extends FormRequest
|
||||
return [
|
||||
'image_digest' => ['required', 'string', 'starts_with:sha256:'],
|
||||
'backup_requested' => ['sometimes', 'boolean'],
|
||||
'confirmation' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
34
app/Http/Requests/UpdateApplicationRequest.php
Normal file
34
app/Http/Requests/UpdateApplicationRequest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\RepositoryType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateApplicationRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'source_provider_id' => ['nullable', 'integer', 'exists:source_providers,id'],
|
||||
'repository_type' => ['required', Rule::enum(RepositoryType::class)],
|
||||
'repository_url' => ['required', 'string', 'max:255', 'regex:/^(git@[^:]+:.+|ssh:\/\/.+)$/i'],
|
||||
'default_branch' => ['required', 'string', 'max:255', 'regex:/^[A-Za-z0-9._\/-]+$/'],
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Http/Requests/UpdateEnvironmentAttachmentRequest.php
Normal file
36
app/Http/Requests/UpdateEnvironmentAttachmentRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\EnvironmentAttachmentRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateEnvironmentAttachmentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'role' => ['required', Rule::enum(EnvironmentAttachmentRole::class)],
|
||||
'env_prefix' => ['nullable', 'string', 'max:255', 'regex:/^[A-Z][A-Z0-9_]*$/'],
|
||||
'is_primary' => ['boolean'],
|
||||
'domain' => ['nullable', 'string', 'max:255'],
|
||||
'path_prefix' => ['nullable', 'string', 'max:255'],
|
||||
'tls_enabled' => ['boolean'],
|
||||
'certificate_status' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
42
app/Http/Requests/UpdateEnvironmentRequest.php
Normal file
42
app/Http/Requests/UpdateEnvironmentRequest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\BuildStrategy;
|
||||
use App\Enums\SchedulerMode;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateEnvironmentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'branch' => ['required', 'string', 'max:255', 'regex:/^[A-Za-z0-9._\/-]+$/'],
|
||||
'status' => ['required', 'string', 'max:255'],
|
||||
'scheduler_enabled' => ['boolean'],
|
||||
'scheduler_target_service_id' => ['nullable', 'integer'],
|
||||
'scheduler_mode' => ['required', Rule::enum(SchedulerMode::class)],
|
||||
'build_strategy' => ['required', Rule::enum(BuildStrategy::class)],
|
||||
'php_version' => ['nullable', 'string', 'max:20'],
|
||||
'document_root' => ['nullable', 'string', 'max:255'],
|
||||
'health_path' => ['nullable', 'string', 'max:255'],
|
||||
'js_package_manager' => ['nullable', 'string', 'max:50'],
|
||||
'js_build_command' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/UpdateEnvironmentVariableRequest.php
Normal file
30
app/Http/Requests/UpdateEnvironmentVariableRequest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateEnvironmentVariableRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'key' => ['required', 'string', 'max:255', 'regex:/^[A-Z][A-Z0-9_]*$/'],
|
||||
'value' => ['nullable', 'string'],
|
||||
'overridable' => ['boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/UpdateGatewayRouteRequest.php
Normal file
31
app/Http/Requests/UpdateGatewayRouteRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateGatewayRouteRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'domain' => ['required', 'string', 'max:255'],
|
||||
'path_prefix' => ['required', 'string', 'max:255', 'regex:/^\//'],
|
||||
'tls_enabled' => ['boolean'],
|
||||
'certificate_status' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/UpdateOrganisationInvitationRequest.php
Normal file
30
app/Http/Requests/UpdateOrganisationInvitationRequest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\OrganisationRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateOrganisationInvitationRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'role' => ['required', Rule::enum(OrganisationRole::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/UpdateOrganisationMemberRequest.php
Normal file
30
app/Http/Requests/UpdateOrganisationMemberRequest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\OrganisationRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateOrganisationMemberRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'role' => ['required', Rule::enum(OrganisationRole::class)],
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Http/Requests/UpdateRegistryRequest.php
Normal file
34
app/Http/Requests/UpdateRegistryRequest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\RegistryType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateRegistryRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', Rule::enum(RegistryType::class)],
|
||||
'url' => ['required', 'string', 'max:255'],
|
||||
'username' => ['nullable', 'string', 'max:255'],
|
||||
'password' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\DeployPolicy;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateServiceRequest extends FormRequest
|
||||
{
|
||||
@@ -26,6 +28,16 @@ class UpdateServiceRequest extends FormRequest
|
||||
'desired_replicas' => ['required', 'integer', 'min:0', 'max:25'],
|
||||
'default_cpu_limit' => ['nullable', 'numeric', 'min:0.125', 'max:64'],
|
||||
'default_memory_limit_mb' => ['nullable', 'integer', 'min:64', 'max:1048576'],
|
||||
'deploy_policy' => ['nullable', Rule::enum(DeployPolicy::class)],
|
||||
'version_track' => ['nullable', 'string', 'max:255'],
|
||||
'available_image_digest' => ['nullable', 'string', 'max:255'],
|
||||
'process_roles' => ['nullable', 'string', 'max:255'],
|
||||
'migration_mode' => ['nullable', 'string', 'max:255'],
|
||||
'migration_timing' => ['nullable', 'string', 'max:255'],
|
||||
'migration_command' => ['nullable', 'string', 'max:255'],
|
||||
'health_path' => ['nullable', 'string', 'max:255'],
|
||||
'backup_enabled' => ['sometimes', 'boolean'],
|
||||
'backup_command' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
32
app/Http/Requests/UpdateSourceProviderRequest.php
Normal file
32
app/Http/Requests/UpdateSourceProviderRequest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\SourceProviderType;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateSourceProviderRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', Rule::enum(SourceProviderType::class)],
|
||||
'url' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use App\Models\Operation;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceReplica;
|
||||
use App\Services\Compose\ComposeRenderer;
|
||||
use App\Support\CaddyRouteRenderer;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use InvalidArgumentException;
|
||||
@@ -29,6 +30,7 @@ class DeployEnvironment implements ShouldQueue
|
||||
|
||||
public function __construct(
|
||||
public Environment $environment,
|
||||
public ?string $targetCommit = null,
|
||||
) {
|
||||
//
|
||||
}
|
||||
@@ -51,7 +53,7 @@ class DeployEnvironment implements ShouldQueue
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$commitSha = app(ResolveEnvironmentCommit::class)->execute($this->environment);
|
||||
$commitSha = $this->targetCommit ?? app(ResolveEnvironmentCommit::class)->execute($this->environment);
|
||||
$services = $this->servicesNeedingDeployment($plan->services, $commitSha);
|
||||
|
||||
if ($services === []) {
|
||||
@@ -378,15 +380,25 @@ class DeployEnvironment implements ShouldQueue
|
||||
private function gatewayCutoverSteps(EnvironmentAttachment $attachment): array
|
||||
{
|
||||
$containerName = $attachment->service->replicas()->first()?->container_name;
|
||||
$config = $attachment->serviceSlice?->config ?? [];
|
||||
$domain = $config['domain'] ?? null;
|
||||
$tlsEnabled = $config['tls_enabled'] ?? true;
|
||||
$reloadCommand = $containerName
|
||||
? 'docker exec '.escapeshellarg($containerName).' caddy reload --config /etc/caddy/Caddyfile'
|
||||
: "docker compose -f /home/keystone/services/{$attachment->service_id}/compose.yml exec -T {$this->serviceKey($attachment->service)} caddy reload --config /etc/caddy/Caddyfile";
|
||||
$certificateCheck = $tlsEnabled && $domain
|
||||
? 'curl --fail --silent --show-error --head https://'.escapeshellarg($domain).' >/dev/null'
|
||||
: 'true # TLS disabled or no domain configured for this route';
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => 'Validate Caddy route configuration',
|
||||
'script' => 'test -s /home/keystone/gateway/Caddyfile',
|
||||
],
|
||||
[
|
||||
'name' => 'Check TLS certificate status',
|
||||
'script' => $certificateCheck,
|
||||
],
|
||||
[
|
||||
'name' => 'Reload Caddy',
|
||||
'script' => $reloadCommand,
|
||||
@@ -406,15 +418,13 @@ class DeployEnvironment implements ShouldQueue
|
||||
|
||||
private function configureCaddyRouteScript(EnvironmentAttachment $attachment): string
|
||||
{
|
||||
$route = $attachment->serviceSlice?->name ?? $this->environment->name;
|
||||
$upstreams = $this->gatewayUpstreams($attachment);
|
||||
$caddyfile = app(CaddyRouteRenderer::class)->render($attachment, $upstreams);
|
||||
|
||||
return implode("\n", [
|
||||
'mkdir -p /home/keystone/gateway/Caddyfile.d',
|
||||
"cat > /home/keystone/gateway/Caddyfile.d/{$attachment->id}.caddy <<'KEYSTONE_CADDY_ROUTE'",
|
||||
"{$route} {",
|
||||
' reverse_proxy '.implode(' ', $upstreams),
|
||||
'}',
|
||||
$caddyfile,
|
||||
'KEYSTONE_CADDY_ROUTE',
|
||||
'cat /home/keystone/gateway/Caddyfile.d/*.caddy > /home/keystone/gateway/Caddyfile',
|
||||
]);
|
||||
|
||||
@@ -29,6 +29,11 @@ class Application extends Model
|
||||
return $this->belongsTo(Organisation::class);
|
||||
}
|
||||
|
||||
public function sourceProvider(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SourceProvider::class);
|
||||
}
|
||||
|
||||
public function environments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Environment::class);
|
||||
|
||||
@@ -30,6 +30,11 @@ class Organisation extends Model
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function invitations(): HasMany
|
||||
{
|
||||
return $this->hasMany(OrganisationInvitation::class);
|
||||
}
|
||||
|
||||
public function servers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Server::class);
|
||||
|
||||
35
app/Models/OrganisationInvitation.php
Normal file
35
app/Models/OrganisationInvitation.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\OrganisationRole;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class OrganisationInvitation extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\OrganisationInvitationFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'role' => OrganisationRole::class,
|
||||
'accepted_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function organisation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Organisation::class);
|
||||
}
|
||||
|
||||
public function invitedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'invited_by_user_id');
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
use Spatie\Ssh\Ssh;
|
||||
|
||||
class Server extends Model
|
||||
@@ -69,6 +70,11 @@ class Server extends Model
|
||||
)->where('target_type', (new Service)->getMorphClass());
|
||||
}
|
||||
|
||||
public function operations(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Operation::class, 'target');
|
||||
}
|
||||
|
||||
public function sshClient(string $user = 'root'): Ssh
|
||||
{
|
||||
return Ssh::create($user, $this->ipv4)
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use App\Enums\SourceProviderType;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class SourceProvider extends Model
|
||||
{
|
||||
@@ -22,4 +23,9 @@ class SourceProvider extends Model
|
||||
{
|
||||
return $this->belongsTo(Organisation::class);
|
||||
}
|
||||
|
||||
public function applications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Application::class);
|
||||
}
|
||||
}
|
||||
|
||||
38
app/Support/CaddyRouteRenderer.php
Normal file
38
app/Support/CaddyRouteRenderer.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\EnvironmentAttachment;
|
||||
|
||||
class CaddyRouteRenderer
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $upstreams
|
||||
*/
|
||||
public function render(EnvironmentAttachment $attachment, array $upstreams): string
|
||||
{
|
||||
$config = $attachment->serviceSlice?->config ?? [];
|
||||
$domain = $config['domain'] ?? $attachment->serviceSlice?->name ?? $attachment->environment->name;
|
||||
$pathPrefix = $config['path_prefix'] ?? '/';
|
||||
$siteAddress = ($config['tls_enabled'] ?? true) ? $domain : "http://{$domain}";
|
||||
$upstreamTargets = $upstreams === [] ? ['web:80'] : $upstreams;
|
||||
|
||||
if ($pathPrefix === '/') {
|
||||
return implode("\n", [
|
||||
"{$siteAddress} {",
|
||||
' reverse_proxy '.implode(' ', $upstreamTargets),
|
||||
'}',
|
||||
]);
|
||||
}
|
||||
|
||||
$normalizedPath = rtrim($pathPrefix, '/');
|
||||
|
||||
return implode("\n", [
|
||||
"{$siteAddress} {",
|
||||
" handle_path {$normalizedPath}* {",
|
||||
' reverse_proxy '.implode(' ', $upstreamTargets),
|
||||
' }',
|
||||
'}',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user