Files
dockerfile.nyxtex.com/app/Http/Controllers/DockerImageBuilderController.php
RDarius 9fbec77ac7
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
build logs
2026-04-24 14:59:20 +03:00

235 lines
8.1 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Http\Requests\DockerImageBuildRequest;
use App\Jobs\RunDockerImageBuild;
use App\Models\DockerImageBuild;
use Carbon\CarbonInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class DockerImageBuilderController extends Controller
{
/**
* Show the Docker image builder page.
*/
public function index(Request $request): Response
{
return Inertia::render('docker/ImageBuilder', [
'build' => $this->detailedPayload($this->resolveActiveBuild($request)),
]);
}
/**
* Show the Docker image build history page.
*/
public function history(Request $request): Response
{
return Inertia::render('docker/BuildHistory', [
'builds' => $request->user()
->dockerImageBuilds()
->latest('id')
->limit(25)
->get()
->map(fn (DockerImageBuild $dockerImageBuild): array => $this->summaryPayload($dockerImageBuild))
->all(),
]);
}
/**
* Return the latest persisted status for a specific build.
*/
public function show(Request $request, DockerImageBuild $dockerImageBuild): JsonResponse
{
return response()->json([
'build' => $this->detailedPayload(
$this->ownedBuild($request, $dockerImageBuild)->fresh(),
),
]);
}
/**
* Queue a Docker image build from the submitted Dockerfile contents.
*/
public function store(DockerImageBuildRequest $request): RedirectResponse
{
$validated = $request->validated();
$environment = $this->normalizeEnvironment($validated['environment'] ?? []);
$dockerImageBuild = DB::transaction(function () use ($request, $validated, $environment): DockerImageBuild {
$dockerImageBuild = $request->user()->dockerImageBuilds()->create([
'image_name' => $validated['image_name'],
'image_tag' => $validated['image_tag'],
'dockerfile_content' => $validated['dockerfile'],
'environment' => $environment,
'status' => DockerImageBuild::STATUS_QUEUED,
'queued_at' => now(),
]);
RunDockerImageBuild::dispatch($dockerImageBuild->id)->afterCommit();
return $dockerImageBuild;
});
Inertia::flash('toast', [
'type' => 'success',
'message' => __('Docker build queued. Logs will update automatically while the queue worker runs.'),
]);
return to_route('docker-builder.index', [
'build' => $dockerImageBuild->id,
]);
}
/**
* Resolve the currently selected build for the authenticated user.
*
* @throws NotFoundHttpException
*/
protected function resolveActiveBuild(Request $request): ?DockerImageBuild
{
$buildId = $request->integer('build');
if ($buildId > 0) {
return $request->user()
->dockerImageBuilds()
->findOrFail($buildId);
}
return $request->user()
->dockerImageBuilds()
->latest('id')
->first();
}
/**
* Ensure the given build belongs to the authenticated user.
*/
protected function ownedBuild(Request $request, DockerImageBuild $dockerImageBuild): DockerImageBuild
{
abort_unless(
$dockerImageBuild->user_id === $request->user()->getAuthIdentifier(),
404,
);
return $dockerImageBuild;
}
/**
* Normalize environment rows before they are saved.
*
* @param array<int, array{key?: string, value?: string}> $environment
* @return array<int, array{key: string, value: string}>
*/
protected function normalizeEnvironment(array $environment): array
{
return collect($environment)
->map(fn (array $variable): array => [
'key' => trim($variable['key'] ?? ''),
'value' => $variable['value'] ?? '',
])
->filter(fn (array $variable): bool => $variable['key'] !== '' || $variable['value'] !== '')
->values()
->all();
}
/**
* Transform a build into the detailed payload used by the builder page.
*/
protected function detailedPayload(?DockerImageBuild $dockerImageBuild): ?array
{
if ($dockerImageBuild === null) {
return null;
}
return [
'id' => $dockerImageBuild->id,
'image' => $dockerImageBuild->image(),
'image_name' => $dockerImageBuild->image_name,
'image_tag' => $dockerImageBuild->image_tag,
'dockerfile_content' => $dockerImageBuild->dockerfile_content,
'environment' => $dockerImageBuild->environment ?? [],
'environment_count' => count($dockerImageBuild->environment ?? []),
'status' => $dockerImageBuild->status,
'status_label' => Str::headline($dockerImageBuild->status),
'successful' => $dockerImageBuild->successful,
'exit_code' => $dockerImageBuild->exit_code,
'build_output' => $dockerImageBuild->build_output ?? '',
'queued_at' => $dockerImageBuild->queued_at?->toIso8601String(),
'queued_at_human' => $dockerImageBuild->queued_at?->diffForHumans(),
'started_at' => $dockerImageBuild->started_at?->toIso8601String(),
'started_at_human' => $dockerImageBuild->started_at?->diffForHumans(),
'finished_at' => $dockerImageBuild->finished_at?->toIso8601String(),
'finished_at_human' => $dockerImageBuild->finished_at?->diffForHumans(),
'duration' => $this->durationFor(
$dockerImageBuild->started_at,
$dockerImageBuild->finished_at,
),
];
}
/**
* Transform a build into the summary payload used by the history page.
*/
protected function summaryPayload(DockerImageBuild $dockerImageBuild): array
{
$output = trim((string) $dockerImageBuild->build_output);
$dockerfilePreview = collect(preg_split('/\R/', trim($dockerImageBuild->dockerfile_content)) ?: [])
->take(4)
->implode(PHP_EOL);
return [
'id' => $dockerImageBuild->id,
'image' => $dockerImageBuild->image(),
'status' => $dockerImageBuild->status,
'status_label' => Str::headline($dockerImageBuild->status),
'successful' => $dockerImageBuild->successful,
'exit_code' => $dockerImageBuild->exit_code,
'environment_count' => count($dockerImageBuild->environment ?? []),
'queued_at_human' => $dockerImageBuild->queued_at?->diffForHumans(),
'duration' => $this->durationFor(
$dockerImageBuild->started_at,
$dockerImageBuild->finished_at,
),
'dockerfile_preview' => $dockerfilePreview !== ''
? $dockerfilePreview
: 'Dockerfile was empty.',
'output_preview' => $output !== ''
? Str::limit($output, 220)
: null,
];
}
/**
* Render a short duration string for build timing metadata.
*/
protected function durationFor(?CarbonInterface $startedAt, ?CarbonInterface $finishedAt): ?string
{
if ($startedAt === null) {
return null;
}
$totalSeconds = $startedAt->diffInSeconds($finishedAt ?? now());
$hours = intdiv($totalSeconds, 3600);
$minutes = intdiv($totalSeconds % 3600, 60);
$seconds = $totalSeconds % 60;
$parts = collect([
$hours > 0 ? "{$hours}h" : null,
$minutes > 0 ? "{$minutes}m" : null,
$hours === 0 ? "{$seconds}s" : null,
])->filter()->take(2);
return $parts->isNotEmpty()
? $parts->implode(' ')
: '0s';
}
}