Skip to content

Commit 9496998

Browse files
feat(registry): add docker registry credential store and app selector
1 parent 083d745 commit 9496998

File tree

22 files changed

+637
-3
lines changed

22 files changed

+637
-3
lines changed

app/Http/Controllers/Api/ApplicationsController.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,7 @@ private function create_application(Request $request, $type)
932932
if ($return instanceof \Illuminate\Http\JsonResponse) {
933933
return $return;
934934
}
935-
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain'];
935+
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'docker_registry_id', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain'];
936936

937937
$validator = customApiValidator($request->all(), [
938938
'name' => 'string|max:255',
@@ -945,6 +945,7 @@ private function create_application(Request $request, $type)
945945
'is_http_basic_auth_enabled' => 'boolean',
946946
'http_basic_auth_username' => 'string|nullable',
947947
'http_basic_auth_password' => 'string|nullable',
948+
'docker_registry_id' => 'integer|nullable|exists:docker_registries,id',
948949
'autogenerate_domain' => 'boolean',
949950
]);
950951

@@ -2136,7 +2137,7 @@ public function update_by_uuid(Request $request)
21362137
$this->authorize('update', $application);
21372138

21382139
$server = $application->destination->server;
2139-
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override'];
2140+
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'docker_registry_id', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override'];
21402141

21412142
$validationRules = [
21422143
'name' => 'string|max:255',
@@ -2152,6 +2153,7 @@ public function update_by_uuid(Request $request)
21522153
'is_http_basic_auth_enabled' => 'boolean|nullable',
21532154
'http_basic_auth_username' => 'string',
21542155
'http_basic_auth_password' => 'string',
2156+
'docker_registry_id' => 'integer|nullable|exists:docker_registries,id',
21552157
];
21562158
$validationRules = array_merge(sharedDataApplications(), $validationRules);
21572159
$validator = customApiValidator($request->all(), $validationRules);

app/Jobs/ApplicationDeploymentJob.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use App\Models\SwarmDocker;
2020
use App\Notifications\Application\DeploymentFailed;
2121
use App\Notifications\Application\DeploymentSuccess;
22+
use App\Services\DockerImageParser;
2223
use App\Traits\EnvironmentVariableAnalyzer;
2324
use App\Traits\ExecuteRemoteCommand;
2425
use Carbon\Carbon;
@@ -572,6 +573,8 @@ private function deploy_dockerimage_buildpack()
572573
// Save runtime environment variables (including empty .env file if no variables defined)
573574
$this->save_runtime_environment_variables();
574575

576+
$this->login_to_docker_registry_if_needed();
577+
575578
$this->rolling_update();
576579
}
577580

@@ -2783,6 +2786,37 @@ private function pull_latest_image($image)
27832786
);
27842787
}
27852788

2789+
private function login_to_docker_registry_if_needed(): void
2790+
{
2791+
$registry = $this->application->dockerRegistry;
2792+
$username = $registry?->username;
2793+
$password = $registry?->password;
2794+
2795+
if (str($username)->isEmpty() || str($password)->isEmpty()) {
2796+
return;
2797+
}
2798+
2799+
if (str($this->application->docker_registry_image_name)->isEmpty()) {
2800+
return;
2801+
}
2802+
2803+
$imageName = str($this->application->docker_registry_image_name)->before('@sha256')->value();
2804+
$parser = new DockerImageParser;
2805+
$parser->parse($imageName.':latest');
2806+
$registryUrl = $registry?->registry_url ?: $parser->getRegistryUrl();
2807+
$registryArg = $registryUrl ? ' '.escapeshellarg($registryUrl) : '';
2808+
$usernameArg = escapeshellarg($username);
2809+
$passwordArg = escapeshellarg($password);
2810+
2811+
$this->application_deployment_queue->addLogEntry('Logging in to Docker registry for image pull.');
2812+
$this->execute_remote_command(
2813+
[
2814+
executeInDocker($this->deployment_uuid, "echo {$passwordArg} | docker login{$registryArg} -u {$usernameArg} --password-stdin"),
2815+
'hidden' => true,
2816+
]
2817+
);
2818+
}
2819+
27862820
private function build_static_image()
27872821
{
27882822
$this->application_deployment_queue->addLogEntry('----------------------------------------');

app/Livewire/Project/Application/General.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
use App\Actions\Application\GenerateConfig;
66
use App\Models\Application;
7+
use App\Models\DockerRegistry;
78
use App\Support\ValidationPatterns;
89
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
910
use Illuminate\Support\Collection;
11+
use Illuminate\Validation\Rule;
1012
use Livewire\Attributes\Validate;
1113
use Livewire\Component;
1214
use Spatie\Url\Url;
@@ -85,6 +87,11 @@ class General extends Component
8587
#[Validate(['string', 'nullable'])]
8688
public ?string $dockerRegistryImageTag = null;
8789

90+
#[Validate(['nullable', 'integer'])]
91+
public ?int $dockerRegistryId = null;
92+
93+
public ?Collection $dockerRegistries = null;
94+
8895
#[Validate(['string', 'nullable'])]
8996
public ?string $dockerComposeLocation = null;
9097

@@ -198,6 +205,7 @@ protected function rules(): array
198205
'dockerfile' => 'nullable',
199206
'dockerRegistryImageName' => 'nullable',
200207
'dockerRegistryImageTag' => 'nullable',
208+
'dockerRegistryId' => ['nullable', 'integer', Rule::exists('docker_registries', 'id')->where('team_id', currentTeam()->id)],
201209
'dockerfileLocation' => 'nullable',
202210
'dockerComposeLocation' => 'nullable',
203211
'dockerCompose' => 'nullable',
@@ -279,6 +287,7 @@ protected function messages(): array
279287
'dockerfile' => 'Dockerfile',
280288
'dockerRegistryImageName' => 'Docker registry image name',
281289
'dockerRegistryImageTag' => 'Docker registry image tag',
290+
'dockerRegistryId' => 'Docker registry credentials',
282291
'dockerfileLocation' => 'Dockerfile location',
283292
'dockerComposeLocation' => 'Docker compose location',
284293
'dockerCompose' => 'Docker compose',
@@ -364,6 +373,8 @@ public function mount()
364373
$this->dispatch('configurationChanged');
365374
}
366375

376+
$this->dockerRegistries = DockerRegistry::ownedByCurrentTeam(['name', 'registry_url'])->get();
377+
367378
// Sync data from model to properties at the END, after all business logic
368379
// This ensures any modifications to $this->application during mount() are reflected in properties
369380
$this->syncData();
@@ -396,6 +407,7 @@ public function syncData(bool $toModel = false): void
396407
$this->application->dockerfile_target_build = $this->dockerfileTargetBuild;
397408
$this->application->docker_registry_image_name = $this->dockerRegistryImageName;
398409
$this->application->docker_registry_image_tag = $this->dockerRegistryImageTag;
410+
$this->application->docker_registry_id = $this->dockerRegistryId;
399411
$this->application->docker_compose_location = $this->dockerComposeLocation;
400412
$this->application->docker_compose = $this->dockerCompose;
401413
$this->application->docker_compose_raw = $this->dockerComposeRaw;
@@ -448,6 +460,7 @@ public function syncData(bool $toModel = false): void
448460
$this->dockerfileTargetBuild = $this->application->dockerfile_target_build;
449461
$this->dockerRegistryImageName = $this->application->docker_registry_image_name;
450462
$this->dockerRegistryImageTag = $this->application->docker_registry_image_tag;
463+
$this->dockerRegistryId = $this->application->docker_registry_id;
451464
$this->dockerComposeLocation = $this->application->docker_compose_location;
452465
$this->dockerCompose = $this->application->docker_compose;
453466
$this->dockerComposeRaw = $this->application->docker_compose_raw;

app/Livewire/Project/New/DockerImage.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
namespace App\Livewire\Project\New;
44

55
use App\Models\Application;
6+
use App\Models\DockerRegistry;
67
use App\Models\Project;
78
use App\Models\StandaloneDocker;
89
use App\Models\SwarmDocker;
910
use App\Services\DockerImageParser;
11+
use Illuminate\Validation\Rule;
1012
use Livewire\Component;
1113
use Visus\Cuid2\Cuid2;
1214

@@ -18,6 +20,10 @@ class DockerImage extends Component
1820

1921
public string $imageSha256 = '';
2022

23+
public ?int $dockerRegistryId = null;
24+
25+
public $dockerRegistries;
26+
2127
public array $parameters;
2228

2329
public array $query;
@@ -26,6 +32,7 @@ public function mount()
2632
{
2733
$this->parameters = get_route_parameters();
2834
$this->query = request()->query();
35+
$this->dockerRegistries = DockerRegistry::ownedByCurrentTeam(['name', 'registry_url'])->get();
2936
}
3037

3138
/**
@@ -86,6 +93,7 @@ public function submit()
8693
'imageName' => ['required', 'string'],
8794
'imageTag' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9._-]*$/i'],
8895
'imageSha256' => ['nullable', 'string', 'regex:/^[a-f0-9]{64}$/i'],
96+
'dockerRegistryId' => ['nullable', 'integer', Rule::exists('docker_registries', 'id')->where('team_id', currentTeam()->id)],
8997
]);
9098

9199
// Validate that either tag or sha256 is provided, but not both
@@ -142,6 +150,7 @@ public function submit()
142150
'ports_exposes' => 80,
143151
'docker_registry_image_name' => $imageName,
144152
'docker_registry_image_tag' => $imageTag,
153+
'docker_registry_id' => $this->dockerRegistryId,
145154
'environment_id' => $environment->id,
146155
'destination_id' => $destination->id,
147156
'destination_type' => $destination_class,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace App\Livewire\Security\DockerRegistry;
4+
5+
use App\Models\DockerRegistry;
6+
use App\Support\ValidationPatterns;
7+
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
8+
use Livewire\Component;
9+
10+
class Create extends Component
11+
{
12+
use AuthorizesRequests;
13+
14+
public string $name = '';
15+
16+
public ?string $description = null;
17+
18+
public ?string $registryUrl = null;
19+
20+
public ?string $username = null;
21+
22+
public ?string $password = null;
23+
24+
protected function rules(): array
25+
{
26+
return [
27+
'name' => ValidationPatterns::nameRules(),
28+
'description' => ValidationPatterns::descriptionRules(),
29+
'registryUrl' => 'nullable|string',
30+
'username' => 'nullable|string|required_with:password',
31+
'password' => 'nullable|string|required_with:username',
32+
];
33+
}
34+
35+
public function createRegistry()
36+
{
37+
$this->validate();
38+
39+
$username = $this->username !== null ? trim($this->username) : null;
40+
$password = $this->password !== null ? trim($this->password) : null;
41+
$registryUrl = $this->registryUrl !== null ? trim($this->registryUrl) : null;
42+
43+
if ($username === '') {
44+
$username = null;
45+
}
46+
47+
if ($password === '') {
48+
$password = null;
49+
}
50+
51+
if ($registryUrl === '') {
52+
$registryUrl = null;
53+
}
54+
55+
if (($username && ! $password) || ($password && ! $username)) {
56+
$this->addError('username', 'Please provide both username and password for the registry.');
57+
$this->addError('password', 'Please provide both username and password for the registry.');
58+
59+
return;
60+
}
61+
62+
try {
63+
$this->authorize('create', DockerRegistry::class);
64+
65+
DockerRegistry::create([
66+
'name' => $this->name,
67+
'description' => $this->description,
68+
'registry_url' => $registryUrl,
69+
'username' => $username,
70+
'password' => $password,
71+
'team_id' => currentTeam()->id,
72+
]);
73+
74+
return redirectRoute($this, 'security.docker-registry.index');
75+
} catch (\Throwable $e) {
76+
return handleError($e, $this);
77+
}
78+
}
79+
80+
public function render()
81+
{
82+
return view('livewire.security.docker-registry.create');
83+
}
84+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace App\Livewire\Security\DockerRegistry;
4+
5+
use App\Models\DockerRegistry;
6+
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
7+
use Livewire\Component;
8+
9+
class Index extends Component
10+
{
11+
use AuthorizesRequests;
12+
13+
public function render()
14+
{
15+
$this->authorize('viewAny', DockerRegistry::class);
16+
17+
$registries = DockerRegistry::ownedByCurrentTeam(['name', 'uuid', 'description', 'registry_url', 'team_id'])->get();
18+
19+
return view('livewire.security.docker-registry.index', [
20+
'registries' => $registries,
21+
])->layout('components.layout');
22+
}
23+
}

0 commit comments

Comments
 (0)