Refactor code and add additional destinations

This commit is contained in:
Andras Bacsai 2024-02-06 15:05:11 +01:00
parent 10e307f92b
commit 3616fc8ca9
10 changed files with 208 additions and 87 deletions

View File

@ -3,6 +3,8 @@
namespace App\Actions\Application; namespace App\Actions\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\StandaloneDocker;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class StopApplication class StopApplication
@ -10,13 +12,20 @@ class StopApplication
use AsAction; use AsAction;
public function handle(Application $application) public function handle(Application $application)
{ {
$server = $application->destination->server; if ($application->destination->server->isSwarm()) {
if (!$server->isFunctional()) { instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
return 'Server is not functional'; return;
} }
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}" ], $server); $servers = collect([]);
} else { $servers->push($application->destination->server);
$application->additional_networks->map(function ($network) use ($servers) {
$servers->push($network->server);
});
foreach ($servers as $server) {
if (!$server->isFunctional()) {
return 'Server is not functional';
}
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0); $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
if ($containers->count() > 0) { if ($containers->count() > 0) {
foreach ($containers as $container) { foreach ($containers as $container) {
@ -28,20 +37,18 @@ class StopApplication
); );
} }
} }
// TODO: make notification for application
// $application->environment->project->team->notify(new StatusChanged($application)); // $application->environment->project->team->notify(new StatusChanged($application));
} }
// Delete Preview Deployments
$previewDeployments = $application->previews;
foreach ($previewDeployments as $previewDeployment) {
$containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
}
}
} }
// // Delete Preview Deployments
// $previewDeployments = $application->previews;
// foreach ($previewDeployments as $previewDeployment) {
// $containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
// foreach ($containers as $container) {
// $name = str_replace('/', '', $container['Names']);
// instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
// }
// }
} }
} }

View File

@ -249,7 +249,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
// Otherwise built image needs to be pushed before from the build server. // Otherwise built image needs to be pushed before from the build server.
if (!$this->use_build_server) { if (!$this->use_build_server) {
$this->push_to_docker_registry(); if ($this->application->additional_networks->count() > 0) {
$this->push_to_docker_registry(forceFail: true);
} else {
$this->push_to_docker_registry();
}
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
@ -349,7 +353,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} catch (Exception $e) { } catch (Exception $e) {
$this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information."); $this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information.");
if ($forceFail) { if ($forceFail) {
throw $e; throw new RuntimeException($e->getMessage(), 69420);
} }
ray($e); ray($e);
} }
@ -388,7 +392,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function just_restart() private function just_restart()
{ {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
@ -443,7 +447,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->server = $this->build_server; $this->server = $this->build_server;
} }
$dockerfile_base64 = base64_encode($this->application->dockerfile); $dockerfile_base64 = base64_encode($this->application->dockerfile);
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->execute_remote_command( $this->execute_remote_command(
[ [
@ -451,6 +455,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
$this->generate_image_names(); $this->generate_image_names();
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->rolling_update();
return;
}
}
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
@ -462,8 +476,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
$this->dockerImage = $this->application->docker_registry_image_name; $this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag; $this->dockerImageTag = $this->application->docker_registry_image_tag;
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"); ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'");
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.");
$this->generate_image_names(); $this->generate_image_names();
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->generate_compose_file(); $this->generate_compose_file();
@ -481,9 +495,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command; $this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command;
} }
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}.");
} else { } else {
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
} }
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
@ -551,12 +565,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (data_get($this->application, 'dockerfile_location')) { if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location; $this->dockerfile_location = $this->application->dockerfile_location;
} }
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->clone_repository();
$this->set_base_dir(); $this->set_base_dir();
$this->generate_image_names(); $this->generate_image_names();
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->rolling_update();
return;
}
}
$this->clone_repository();
$this->cleanup_git(); $this->cleanup_git();
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
@ -569,7 +593,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->use_build_server) { if ($this->use_build_server) {
$this->server = $this->build_server; $this->server = $this->build_server;
} }
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
@ -601,11 +625,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->use_build_server) { if ($this->use_build_server) {
$this->server = $this->build_server; $this->server = $this->build_server;
} }
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
$this->generate_image_names(); $this->generate_image_names();
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->rolling_update();
return;
}
}
$this->clone_repository(); $this->clone_repository();
$this->cleanup_git(); $this->cleanup_git();
$this->build_image(); $this->build_image();
@ -787,10 +821,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_to_additional_destinations() private function deploy_to_additional_destinations()
{ {
if (str($this->application->additional_destinations)->isEmpty()) { if ($this->application->additional_networks->count() === 0) {
return; return;
} }
$destination_ids = collect(str($this->application->additional_destinations)->explode(',')); $destination_ids = $this->application->additional_networks->pluck('id');
if ($this->server->isSwarm()) { if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode."); $this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode.");
return; return;
@ -1529,7 +1563,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
return; return;
} }
if ($status === ApplicationDeploymentStatus::FINISHED->value) { if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->deploy_to_additional_destinations(); $this->deploy_to_additional_destinations();
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
} }
} }
@ -1542,10 +1576,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} }
if ($this->application->build_pack !== 'dockercompose') { if ($this->application->build_pack !== 'dockercompose') {
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr'); $code = $exception->getCode();
$this->execute_remote_command( if ($code !== 69420) {
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
); $this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
);
}
} }
$this->next(ApplicationDeploymentStatus::FAILED->value); $this->next(ApplicationDeploymentStatus::FAILED->value);

View File

@ -87,22 +87,6 @@ class Heading extends Component
$this->application->save(); $this->application->save();
$this->application->refresh(); $this->application->refresh();
} }
public function restartNew()
{
$this->setDeploymentUuid();
queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
is_new_deployment: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
]);
}
public function restart() public function restart()
{ {
$this->setDeploymentUuid(); $this->setDeploymentUuid();

View File

@ -2,11 +2,43 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Models\Server;
use Livewire\Component; use Livewire\Component;
class Destination extends Component class Destination extends Component
{ {
public $resource; public $resource;
public $servers = []; public $servers = [];
public $additional_servers = []; public $networks = [];
public function mount()
{
$this->loadData();
}
public function loadData()
{
$all_networks = collect([]);
$all_networks = $all_networks->push($this->resource->destination);
$all_networks = $all_networks->merge($this->resource->additional_networks);
$this->networks = Server::isUsable()->get()->map(function ($server) {
return $server->standaloneDockers;
})->flatten();
$this->networks = $this->networks->reject(function ($network) use ($all_networks) {
return $all_networks->pluck('id')->contains($network->id);
});
}
public function addServer(int $network_id, int $server_id)
{
$this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]);
$this->resource->load(['additional_networks']);
$this->loadData();
}
public function removeServer(int $network_id, int $server_id)
{
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
$this->resource->load(['additional_networks']);
$this->loadData();
}
} }

View File

@ -171,7 +171,7 @@ class All extends Component
} }
$environment->save(); $environment->save();
$this->refreshEnvs(); $this->refreshEnvs();
$this->dispatch('success', 'Environment variable added successfully.'); $this->dispatch('success', 'Environment variable added.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@ -15,7 +15,6 @@ class Application extends BaseModel
{ {
use SoftDeletes; use SoftDeletes;
protected $guarded = []; protected $guarded = [];
protected static function booted() protected static function booted()
{ {
static::saving(function ($application) { static::saving(function ($application) {
@ -53,6 +52,16 @@ class Application extends BaseModel
}); });
} }
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
->withPivot('standalone_docker_id');
}
public function additional_networks()
{
return $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')
->withPivot('server_id');
}
public function is_github_based(): bool public function is_github_based(): bool
{ {
if (data_get($this, 'source')) { if (data_get($this, 'source')) {
@ -216,7 +225,8 @@ class Application extends BaseModel
{ {
return $this->morphToMany(Tag::class, 'taggable'); return $this->morphToMany(Tag::class, 'taggable');
} }
public function project() { public function project()
{
return data_get($this, 'environment.project'); return data_get($this, 'environment.project');
} }
public function team() public function team()
@ -435,7 +445,7 @@ class Application extends BaseModel
{ {
return "/artifacts/{$uuid}"; return "/artifacts/{$uuid}";
} }
function setGitImportSettings(string $deployment_uuid, string $git_clone_command) function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
{ {
$baseDir = $this->generateBaseDir($deployment_uuid); $baseDir = $this->generateBaseDir($deployment_uuid);
if ($this->git_commit_sha !== 'HEAD') { if ($this->git_commit_sha !== 'HEAD') {

View File

@ -24,7 +24,6 @@ return new class extends Migration
$table->string('taggable_type'); $table->string('taggable_type');
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
$table->unique(['tag_id', 'taggable_id', 'taggable_type'], 'taggable_unique'); // Composite unique index $table->unique(['tag_id', 'taggable_id', 'taggable_type'], 'taggable_unique'); // Composite unique index
}); });
} }

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('additional_destinations', function (Blueprint $table) {
$table->id();
$table->foreignId('application_id')->constrained()->onDelete('cascade');
$table->foreignId('server_id')->constrained()->onDelete('cascade');
$table->foreignId('standalone_docker_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('additional_destinations');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('additional_destinations');
Schema::table('applications', function (Blueprint $table) {
$table->string('additional_destinations')->nullable()->after('destination');
});
}
};

View File

@ -77,6 +77,11 @@
Manual Manual
</div> </div>
@endif @endif
@if (data_get($deployment, 'server_name'))
<div class="flex gap-1">
Server: {{ data_get($deployment, 'server_name') }}
</div>
@endif
</div> </div>
<div class="flex flex-col" x-data="elapsedTime('{{ $deployment->deployment_uuid }}', '{{ $deployment->status }}', '{{ $deployment->created_at }}', '{{ $deployment->updated_at }}')"> <div class="flex flex-col" x-data="elapsedTime('{{ $deployment->deployment_uuid }}', '{{ $deployment->status }}', '{{ $deployment->created_at }}', '{{ $deployment->updated_at }}')">

View File

@ -1,38 +1,48 @@
<div> <div>
<h2>Server</h2> <h2>Server</h2>
<div class="">Server related configurations.</div> <div class="">Server related configurations.</div>
<h3 class="pt-4">Destination Server & Network</h3> <div class="grid grid-cols-2 gap-4 py-4">
<div class="py-4"> {{-- <a class="box"
<a class="box"
href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}">On href="{{ route('server.show', ['server_uuid' => data_get($resource, 'destination.server.uuid')]) }}">On
server <span class="px-1 text-warning">{{ data_get($resource, 'destination.server.name') }}</span> server <span class="px-1 text-warning">{{ data_get($resource, 'destination.server.name') }}</span>
in <span class="px-1 text-warning"> {{ data_get($resource, 'destination.network') }} </span> network.</a> in <span class="px-1 text-warning"> {{ data_get($resource, 'destination.network') }} </span> network</a>
@if (count($additional_destinations) > 0)
@foreach ($additional_destinations as $destination)
<a class="box"
href="{{ route('server.show', ['server_uuid' => data_get($destination, 'server.uuid')]) }}">On server
<span class="px-1 text-warning">{{ data_get($destination, 'server.name') }}</span> in <span
class="px-1 text-warning"> {{ data_get($destination, 'network') }} </span> network</a>
@endforeach
@endif --}}
<div class="box"
wire:click="removeServer('{{ data_get($resource, 'destination.id') }}','{{ data_get($resource, 'destination.server.id') }}')">
On
server <span class="px-1 text-warning">{{ data_get($resource, 'destination.server.name') }}</span>
in <span class="px-1 text-warning"> {{ data_get($resource, 'destination.network') }} </span> network</div>
@if (count($resource->additional_networks) > 0)
@foreach ($resource->additional_networks as $destination)
<div class="box"
wire:click="removeServer('{{ data_get($destination, 'id') }}','{{ data_get($destination, 'server.id') }}')">
On
server
<span class="px-1 text-warning">{{ data_get($destination, 'server.name') }}</span> in <span
class="px-1 text-warning"> {{ data_get($destination, 'network') }} </span> network
</div>
@endforeach
@endif
</div> </div>
{{-- Additional Destinations: <h4>Attach to a Server</h4>
{{$resource->additional_destinations}} --}} @if (count($networks) > 0)
{{-- @if (count($servers) > 0) <div class="grid grid-cols-2 gap-4">
<div> @foreach ($networks as $network)
<h3>Additional Servers</h3> <div wire:click="addServer('{{ $network->id }}','{{ data_get($network, 'server.id') }}')"
@foreach ($servers as $server) class="box">
<form wire:submit='submit' class="p-2 border border-coolgray-400"> {{ data_get($network, 'server.name') }}
<h4>{{ $server->name }}</h4> {{ $network->name }}
<div class="text-sm text-coolgray-600">{{ $server->description }}</div> </div>
<x-forms.checkbox id="additionalServers.{{ $loop->index }}.enabled" label="Enabled">
</x-forms.checkbox>
<x-forms.select label="Destination" id="additionalServers.{{ $loop->index }}.destination" required>
@foreach ($server->destinations() as $destination)
@if ($loop->first)
<option selected value="{{ $destination->uuid }}">{{ $destination->name }}</option>
<option value="{{ $destination->uuid }}">{{ $destination->name }}</option>
@else
<option value="{{ $destination->uuid }}">{{ $destination->name }}</option>
<option value="{{ $destination->uuid }}">{{ $destination->name }}</option>
@endif
@endforeach
</x-forms.select>
<x-forms.button type="submit">Save</x-forms.button>
</form>
@endforeach @endforeach
</div> </div>
@endif --}} @else
<div class="text-neutral-500">No additional servers available to attach.</div>
@endif
</div> </div>