235 lines
8.1 KiB
PHP
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';
|
|
}
|
|
}
|