chore: database, navigation images

This commit is contained in:
David Buday 2024-11-11 15:35:25 +01:00
parent 3cd185aa78
commit 42de7ec0f0
20 changed files with 332 additions and 58 deletions

View file

@ -9,6 +9,7 @@ use App\Events\ApplicationStatusChanged;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\DockerRegistry;
use App\Models\EnvironmentVariable;
use App\Models\GithubApp;
use App\Models\GitlabApp;
@ -383,6 +384,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function deploy_dockerimage_buildpack()
{
$useCustomRegistry = $this->application->docker_use_custom_registry;
try {
// setup
$this->dockerImage = $this->application->docker_registry_image_name;
@ -394,7 +396,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.");
// login if use custom registry
if ($this->application->docker_use_custom_registry) {
if ($useCustomRegistry) {
$this->handleRegistryAuth();
}
@ -402,25 +404,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->prepare_builder_image();
$this->generate_compose_file();
$this->rolling_update();
// Logout if use custom registry
if ($this->application->docker_use_custom_registry) {
} catch (Exception $e) {
throw $e;
} finally {
if ($useCustomRegistry) {
$this->application_deployment_queue->addLogEntry('Logging out from registry...');
$this->execute_remote_command([
'docker logout',
'hidden' => true
]);
}
} catch (Exception $e) {
// Make sure to logout even if build/pull fails
if ($this->application->docker_use_custom_registry) {
$this->execute_remote_command([
'docker logout',
'hidden' => true
]);
}
//$this->application_deployment_queue->addLogEntry('Deployment error: ' . $e->getMessage(), 'stderr');
throw $e;
}
}
@ -2477,13 +2470,27 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function handleRegistryAuth()
{
//$registry = $this->$application->registry; ??
//$token = escapeshellarg($registry->token); ...
$token = escapeshellarg('test');
$url = escapeshellarg('test');
$username = escapeshellarg('test');
$registry = DockerRegistry::find($this->application->docker_registry_id);
if (!$registry) {
throw new Exception('Registry not found.');
}
$token = escapeshellarg($registry->token);
$username = escapeshellarg($registry->username);
// Handle different registry types
$url = match ($registry->type) {
'docker_hub' => '', // Docker Hub doesn't need URL specified
'custom' => escapeshellarg($registry->url),
default => escapeshellarg($registry->url)
};
$this->application_deployment_queue->addLogEntry('Attempting to log into registry...');
$command = "echo {{secrets.token}} | docker login {$url} -u {$username} --password-stdin";
// Build login command based on registry type
$command = $registry->type === 'docker_hub'
? "echo {{secrets.token}} | docker login -u {$username} --password-stdin"
: "echo {{secrets.token}} | docker login {$url} -u {$username} --password-stdin";
$this->execute_remote_command(
[

View file

View file

@ -0,0 +1,13 @@
<?php
namespace App\Livewire\Images\Images;
use Livewire\Component;
class Index extends Component
{
public function render()
{
return view('livewire.images.images.index');
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Livewire\Images\Registry;
use App\Models\DockerRegistry;
use Livewire\Component;
class Create extends Component
{
public string $name = '';
public string $type = 'docker_hub';
public ?string $url = null;
public ?string $username = null;
public ?string $token = null;
protected $rules = [
'name' => 'required|string|max:255',
'type' => 'required|string',
'url' => 'nullable|string|max:255',
'username' => 'nullable|string|max:255',
'token' => 'nullable|string',
];
public function getRegistryTypesProperty()
{
return DockerRegistry::getTypes();
}
public function submit()
{
$this->validate();
DockerRegistry::create([
'name' => $this->name,
'type' => $this->type,
'url' => $this->type === 'custom' ? $this->url : 'docker.io',
'username' => $this->username,
'token' => $this->token,
]);
$this->dispatch('registry-added');
$this->dispatch('success', 'Registry added successfully.');
$this->dispatch('close-modal');
}
public function render()
{
return view('livewire.images.registry.create');
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Livewire\Images\Registry;
use App\Models\DockerRegistry;
use Livewire\Component;
class Index extends Component
{
protected $listeners = ['registry-added' => '$refresh'];
public function render()
{
return view('livewire.images.registry.index', [
'registries' => DockerRegistry::all()
]);
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace App\Livewire\Images\Registry;
use App\Models\DockerRegistry;
use Livewire\Component;
class Show extends Component
{
public DockerRegistry $registry;
public string $name = '';
public string $type = '';
public ?string $url = null;
public ?string $username = null;
public ?string $token = null;
protected $rules = [
'name' => 'required|string|max:255',
'type' => 'required|string',
'url' => 'nullable|string|max:255',
'username' => 'nullable|string|max:255',
'token' => 'nullable|string',
];
public function mount(DockerRegistry $registry)
{
$this->registry = $registry;
$this->name = $registry->name;
$this->type = $registry->type;
$this->url = $registry->url;
$this->username = $registry->username;
$this->token = $registry->token;
}
public function getRegistryTypesProperty()
{
return DockerRegistry::getTypes();
}
public function updateRegistry()
{
$this->validate();
$this->registry->update([
'name' => $this->name,
'type' => $this->type,
'url' => $this->type === 'custom' ? $this->url : 'docker.io',
'username' => $this->username,
'token' => $this->token,
]);
$this->dispatch('success', 'Registry updated successfully.');
}
public function delete()
{
// Update all applications using this registry
$this->registry->applications()
->update([
'docker_registry_id' => null,
'docker_use_custom_registry' => false
]);
$this->registry->delete();
$this->dispatch('registry-added');
$this->dispatch('success', 'Registry deleted successfully.');
}
public function render()
{
return view('livewire.images.registry.show');
}
public function getIsFormDirtyProperty(): bool
{
return $this->name !== $this->registry->name
|| $this->type !== $this->registry->type
|| $this->url !== $this->registry->url
|| $this->username !== $this->registry->username
|| $this->token !== $this->registry->token;
}
}

View file

@ -4,7 +4,7 @@ namespace App\Livewire\Project\Application;
use App\Actions\Application\GenerateConfig;
use App\Models\Application;
use App\Models\Registry;
use App\Models\DockerRegistry;
use Illuminate\Support\Collection;
use Livewire\Component;
use Spatie\Url\Url;
@ -444,7 +444,7 @@ class General extends Component
public function render()
{
return view('livewire.project.application.general', [
'registries' => Registry::all(),
'registries' => DockerRegistry::all(),
]);
}
}

View file

@ -3,7 +3,7 @@
namespace App\Livewire\Project\New;
use App\Models\Application;
use App\Models\Registry;
use App\Models\DockerRegistry;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
@ -20,8 +20,7 @@ class DockerImage extends Component
protected $rules = [
'dockerImage' => 'required|string',
'selectedRegistry' => 'nullable|required_if:useCustomRegistry,true',
'useCustomRegistry' => 'boolean'
'selectedRegistry' => 'required_if:useCustomRegistry,true|nullable|exists:docker_registries,id'
];
public function mount()
@ -62,7 +61,7 @@ class DockerImage extends Component
'docker_registry_image_name' => $image,
'docker_registry_image_tag' => $tag,
'docker_use_custom_registry' => $this->useCustomRegistry,
'docker_registry_id' => $this->selectedRegistry,
'docker_registry_id' => $this->selectedRegistry ?? null,
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
@ -85,7 +84,7 @@ class DockerImage extends Component
public function render()
{
return view('livewire.project.new.docker-image', [
'registries' => Registry::all()
'registries' => DockerRegistry::all()
]);
}
}

View file

@ -1536,6 +1536,6 @@ class Application extends BaseModel
public function registry()
{
return $this->belongsTo(Registry::class);
return $this->belongsTo(DockerRegistry::class);
}
}

View file

@ -5,16 +5,10 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Registry extends Model
// #[OA\Schema(
class DockerRegistry extends Model
{
protected $fillable = [
'name',
'type',
'url',
'username',
'token',
'is_default'
];
protected $guarded = [];
protected $casts = [
'is_default' => 'boolean',
@ -34,6 +28,6 @@ class Registry extends Model
public function applications(): HasMany
{
return $this->hasMany(Application::class);
return $this->hasMany(Application::class, 'docker_registry_id', 'id');
}
}

View file

@ -14,7 +14,6 @@ return new class extends Migration {
$table->string('url')->nullable();
$table->string('username')->nullable();
$table->text('token')->nullable();
$table->boolean('is_default')->default(false);
$table->timestamps();
});

View file

@ -0,0 +1,14 @@
<div class="pb-6">
<h1>Images</h1>
<div class="subtitle">Images and container management.</div>
<div class="navbar-main">
<nav class="flex items-center gap-6 scrollbar min-h-10">
<a href="{{ route('images.images.index') }}">
<button>Images</button>
</a>
<a href="{{ route('images.registries.index') }}">
<button>Registries</button>
</a>
</nav>
</div>
</div>

View file

@ -145,6 +145,16 @@
Sources
</a>
</li>
<li>
<a title="Images"
class="{{ request()->is('images*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('images.images.index') }}" <svg xmlns="http://www.w3.org/2000/svg"
class="icon" viewBox="0 0 24 24">
<!-- docker svg -->
</svg>
Images
</a>
</li>
<li>
<a title="Destinations"
class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-item' }}"
@ -163,8 +173,8 @@
class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('storage.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<g fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path d="M4 6a8 3 0 1 0 16 0A8 3 0 1 0 4 6" />
<path d="M4 6v6a8 3 0 0 0 16 0V6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />

View file

@ -0,0 +1,16 @@
<div>
<x-images.navbar />
<div class="flex gap-2">
<h2 class="pb-4">Images</h2>
<x-modal-input buttonTitle="+ Add" disabled title="New Image">
<livewire:security.private-key.create />
</x-modal-input>
</div>
{{-- Images Tab Content --}}
<div x-show="$wire.activeTab === 'images'" x-cloak>
<div class="text-gray-500">
Image management coming soon...
</div>
</div>
</div>

View file

@ -0,0 +1,23 @@
<form wire:submit="submit" class="flex flex-col gap-4">
<x-forms.input wire:model="name" required id="name" label="Registry Name" placeholder="My Docker Hub" />
<x-forms.select wire:model.live="type" label="Registry Type">
@foreach ($this->registryTypes as $key => $value)
<option value="{{ $key }}">{{ $value }}</option>
@endforeach
</x-forms.select>
@if ($type === 'custom')
<x-forms.input wire:model="url" required id="url" label="Registry URL"
placeholder="registry.example.com" />
@endif
<x-forms.input wire:model="username" id="username" label="Username" placeholder="Username for authentication" />
<x-forms.input wire:model="token" type="password" id="token" label="Token/Password"
placeholder="Authentication token or password" />
<div class="flex justify-end gap-2">
<x-forms.button type="submit">Save Registry</x-forms.button>
</div>
</form>

View file

@ -0,0 +1,18 @@
<div class="space-y-4">
<x-images.navbar />
<div class="flex items-center gap-2">
<h2>Registries</h2>
<x-modal-input buttonTitle="+ Add" title="New Registry">
<livewire:images.registry.create />
</x-modal-input>
</div>
<div>Configure registries to pull Docker images from.</div>
@forelse($registries as $registry)
<livewire:images.registry.show :registry="$registry" wire:key="registry-{{ $registry->id }}" />
@empty
<div class="text-center py-8 text-gray-500">
No registries configured yet. Add one to get started.
</div>
@endforelse
</div>

View file

@ -0,0 +1,32 @@
<div class="flex flex-col p-4 bg-white dark:bg-base border border-coolgray-200 dark:border-coolgray-700">
<form wire:submit="updateRegistry" class="space-y-4">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<x-forms.button type="submit">Update</x-forms.button>
<x-modal-confirmation title="Confirm Registry Deletion?" isErrorButton buttonTitle="Delete"
submitAction="delete" :actions="['The selected registry will be permanently deleted.']" confirmationText="{{ $registry->name }}"
confirmationLabel="Please confirm by entering the registry name"
shortConfirmationLabel="Registry Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<x-forms.input wire:model="name" label="Name" required />
<x-forms.select wire:model.live="type" label="Type">
@foreach ($this->registryTypes as $key => $value)
<option value="{{ $key }}">{{ $value }}</option>
@endforeach
</x-forms.select>
@if ($type === 'custom')
<x-forms.input wire:model="url" label="URL" placeholder="registry.example.com" required />
@endif
<x-forms.input wire:model="username" label="Username" placeholder="Username for authentication" />
<x-forms.input wire:model="token" type="password" label="Token/Password"
placeholder="Authentication token or password" />
</div>
</form>
</div>

View file

@ -144,7 +144,9 @@
@if ($application->docker_use_custom_registry)
<div class="pt-4">
<x-forms.select id="application.docker_registry_id" label="Select Registry">
<x-forms.select id="application.docker_registry_id" label="Select Registry"
required="required_if:application.docker_use_custom_registry,true">
{{-- <option>Select a registry...</option> --}}
@foreach ($registries as $registry)
<option value="{{ $registry['id'] }}">{{ $registry['name'] }}</option>
@endforeach

View file

@ -10,23 +10,17 @@
<div class="pt-4 w-fit">
<x-forms.checkbox wire:model.live="useCustomRegistry" id="useCustomRegistry"
helper="If enabled, you can specify a custom registry URL, username, and token/password."
label="Use Custom Registry Settings" />
helper="Select a registry to pull the image from." label="Use Private Registry" />
</div>
@if ($useCustomRegistry)
<h3 class="pt-4">Registry Authentication</h3>
<div class="flex flex-col gap-4">
<x-forms.input id="registryUrl" label="Registry URL" placeholder="registry.example.com"
helper="Leave empty for Docker Hub" />
<x-forms.input id="registryUsername" label="Registry Username"
required="required_if:useCustomRegistry,true" placeholder="Username for private registry"
helper="Leave empty for public images or server credentials" />
<x-forms.input type="password" id="registryToken" label="Registry Token/Password"
required="required_if:useCustomRegistry,true" placeholder="Token or password for private registry"
helper="Leave empty for public images or server credentials" />
<div class="pt-4">
<x-forms.select id="selectedRegistry" wire:model="selectedRegistry" label="Select Registry" required>
<option value="">Select a registry...</option>
@foreach ($registries as $registry)
<option value="{{ $registry->id }}">{{ $registry->name }}</option>
@endforeach
</x-forms.select>
</div>
@endif
</form>

View file

@ -33,6 +33,8 @@ use App\Livewire\Project\Shared\ExecuteContainerCommand;
use App\Livewire\Project\Shared\Logs;
use App\Livewire\Project\Shared\ScheduledTask\Show as ScheduledTaskShow;
use App\Livewire\Project\Show as ProjectShow;
use App\Livewire\Images\Registry\Index as RegistryIndex;
use App\Livewire\Images\Images\Index as ImagesIndex;
use App\Livewire\Security\ApiTokens;
use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex;
use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow;
@ -228,6 +230,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/security/private-key/{private_key_uuid}', SecurityPrivateKeyShow::class)->name('security.private-key.show');
Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens');
Route::get('/images/images', ImagesIndex::class)->name('images.images.index');
Route::get('/images/registries', RegistryIndex::class)->name('images.registries.index');
});
Route::middleware(['auth'])->group(function () {
@ -306,13 +310,12 @@ Route::middleware(['auth'])->group(function () {
fclose($stream);
}, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.basename($filename).'"',
'Content-Disposition' => 'attachment; filename="' . basename($filename) . '"',
]);
} catch (\Throwable $e) {
return response()->json(['message' => $e->getMessage()], 500);
}
})->name('download.backup');
});
Route::any('/{any}', function () {