build logs
This commit is contained in:
234
app/Http/Controllers/DockerImageBuilderController.php
Normal file
234
app/Http/Controllers/DockerImageBuilderController.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user