fix: multiple server deployments

feat: custom preview deployment fqdn
ui: improvements here and there
This commit is contained in:
Andras Bacsai 2024-05-30 12:28:29 +02:00
parent 07e801f44d
commit 68d3cea528
14 changed files with 314 additions and 151 deletions

View File

@ -67,6 +67,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Save original server between phases
private Server $original_server;
private Server $mainServer;
private bool $is_this_additional_server = false;
private ?ApplicationPreview $preview = null;
private ?string $git_type = null;
private bool $only_this_server = false;
@ -124,6 +125,7 @@ public function __construct(int $application_deployment_queue_id)
$this->rollback = $this->application_deployment_queue->rollback;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
$this->git_type = data_get($this->application_deployment_queue, 'git_type');
@ -137,6 +139,8 @@ public function __construct(int $application_deployment_queue_id)
$this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first();
$this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user;
$this->is_this_additional_server = $this->application->additional_servers()->wherePivot('server_id', $this->server->id)->count() > 0;
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
@ -150,28 +154,7 @@ public function __construct(int $application_deployment_queue_id)
// Set preview fqdn
if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
if ($this->application->fqdn) {
if (str($this->application->fqdn)->contains(',')) {
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
} else {
$url = Url::fromString($this->application->fqdn);
if (data_get($this->preview, 'fqdn')) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
}
}
$template = $this->application->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
$random = new Cuid2(7);
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
$preview_fqdn = "$schema://$preview_fqdn";
$this->preview->fqdn = $preview_fqdn;
$this->preview->save();
}
$this->preview = $this->application->generate_preview_fqdn($this->pull_request_id);
if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS);
}
@ -285,7 +268,7 @@ public function handle(): void
}
private function decide_what_to_do()
{
if ($this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile') {
if ($this->restart_only) {
$this->just_restart();
return;
} else if ($this->pull_request_id !== 0) {
@ -335,18 +318,6 @@ private function deploy_simple_dockerfile()
],
);
$this->generate_image_names();
// Always rebuild dockerfile based container.
// if (!$this->force_rebuild) {
// $this->check_image_locally_or_remotely();
// if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
// $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->push_to_docker_registry();
// $this->rolling_update();
// return;
// }
// }
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
@ -406,6 +377,11 @@ private function deploy_docker_compose_buildpack()
});
$composeFile['services'] = $services->toArray();
}
if (is_null($composeFile)) {
$this->application_deployment_queue->addLogEntry("Failed to parse docker-compose file.");
$this->fail("Failed to parse docker-compose file.");
return;
}
$yaml = Yaml::dump($composeFile->toArray(), 10);
}
$this->docker_compose_base64 = base64_encode($yaml);
@ -499,16 +475,11 @@ private function deploy_dockerfile_buildpack()
}
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->clone_repository();
if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$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->push_to_docker_registry();
$this->rolling_update();
if ($this->should_skip_build()) {
return;
}
}
@ -528,21 +499,12 @@ private function deploy_nixpacks_buildpack()
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$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->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();
ray('pushing to docker registry');
$this->push_to_docker_registry();
$this->rolling_update();
if ($this->should_skip_build()) {
return;
}
if ($this->application->isConfigurationChanged()) {
$this->application_deployment_queue->addLogEntry("Configuration changed. Rebuilding image.");
}
}
$this->clone_repository();
$this->cleanup_git();
@ -561,15 +523,10 @@ private function deploy_static_buildpack()
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$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->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->push_to_docker_registry();
$this->rolling_update();
if ($this->should_skip_build()) {
return;
}
}
@ -637,7 +594,7 @@ private function push_to_docker_registry()
ray('additional_servers');
$forceFail = true;
}
if ($this->application->additional_servers()->wherePivot('server_id', $this->server->id)->count() > 0) {
if ($this->is_this_additional_server) {
ray('this is an additional_servers, no pushy pushy');
return;
}
@ -652,8 +609,8 @@ private function push_to_docker_registry()
],
);
if ($this->application->docker_registry_image_tag) {
// Tag image with latest
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with latest tag.");
// Tag image with docker_registry_image_tag
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with {$this->application->docker_registry_image_tag} tag.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
@ -694,9 +651,9 @@ private function generate_image_names()
}
} else {
$this->dockerImageTag = str($this->commit)->substr(0, 128);
if ($this->application->docker_registry_image_tag) {
$this->dockerImageTag = $this->application->docker_registry_image_tag;
}
// if ($this->application->docker_registry_image_tag) {
// $this->dockerImageTag = $this->application->docker_registry_image_tag;
// }
if ($this->application->docker_registry_image_name) {
$this->build_image_name = "{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build";
$this->production_image_name = "{$this->application->docker_registry_image_name}:{$this->dockerImageTag}";
@ -711,19 +668,42 @@ private function just_restart()
$this->application_deployment_queue->addLogEntry("Restarting {$this->customRepository}:{$this->application->git_branch} on {$this->server->name}.");
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->check_image_locally_or_remotely();
if ($this->should_skip_build()) {
return;
}
}
private function should_skip_build()
{
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.");
$this->generate_compose_file();
$this->rolling_update();
$this->post_deployment();
if ($this->is_this_additional_server) {
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->push_to_docker_registry();
$this->rolling_update();
if ($this->restart_only) {
$this->post_deployment();
}
return true;
}
if (!$this->application->isConfigurationChanged()) {
$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->push_to_docker_registry();
$this->rolling_update();
return true;
} else {
$this->application_deployment_queue->addLogEntry("Configuration changed. Rebuilding image.");
}
} else {
$this->application_deployment_queue->addLogEntry("Image not found ({$this->production_image_name}). Redeploying the application.");
$this->application_deployment_queue->addLogEntry("Image not found ({$this->production_image_name}). Building new image.");
}
if ($this->restart_only) {
$this->restart_only = false;
$this->decide_what_to_do();
}
return false;
}
private function check_image_locally_or_remotely()
{
@ -890,7 +870,6 @@ private function save_environment_variables()
]
);
}
}
}
@ -1057,7 +1036,6 @@ private function deploy_pull_request()
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
$this->set_base_dir();
$this->cleanup_git();
if ($this->application->build_pack === 'nixpacks') {
$this->generate_nixpacks_confs();
@ -1179,10 +1157,6 @@ private function deploy_to_additional_destinations()
]));
}
}
private function set_base_dir()
{
$this->application_deployment_queue->addLogEntry("Setting base directory to {$this->workdir}.");
}
private function set_coolify_variables()
{
$this->coolify_variables = "SOURCE_COMMIT={$this->commit} ";

View File

@ -287,7 +287,7 @@ public function checkFqdns($showToaster = true)
if ($this->application->additional_servers->count() === 0) {
foreach ($domains as $domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$showToaster && $this->dispatch('error', "Validating DNS failed.", "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
}
}
@ -352,7 +352,7 @@ public function submit($showToaster = true)
$domain = data_get($service, 'domain');
if ($domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) {
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$showToaster && $this->dispatch('error', "Validating DNS failed.", "Make sure you have added the DNS records correctly.<br><br>$domain->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
}
check_domain_usage(resource: $this->application);
}

View File

@ -2,10 +2,12 @@
namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Illuminate\Support\Collection;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
class Previews extends Component
@ -16,6 +18,9 @@ class Previews extends Component
public Collection $pull_requests;
public int $rate_limit_remaining;
protected $rules = [
'application.previews.*.fqdn' => 'string|nullable',
];
public function mount()
{
$this->pull_requests = collect();
@ -33,7 +38,71 @@ public function load_prs()
return handleError($e, $this);
}
}
public function save_preview($preview_id)
{
try {
$success = true;
$preview = $this->application->previews->find($preview_id);
if (isset($preview->fqdn)) {
$preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim();
$preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim();
$preview->fqdn = str($preview->fqdn)->trim()->lower();
if (!validate_dns_entry($preview->fqdn, $this->application->destination->server)) {
$this->dispatch('error', "Validating DNS failed.", "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$success = false;
}
check_domain_usage(resource: $this->application, domain: $preview->fqdn);
}
if (!$preview) {
throw new \Exception('Preview not found');
}
$success && $preview->save();
$success && $this->dispatch('success', 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function generate_preview($preview_id)
{
$preview = $this->application->previews->find($preview_id);
if (!$preview) {
$this->dispatch('error', 'Preview not found.');
return;
}
$fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
$url = Url::fromString($fqdn);
$template = $this->application->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
$random = new Cuid2(7);
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $preview_id, $preview_fqdn);
$preview_fqdn = "$schema://$preview_fqdn";
$preview->fqdn = $preview_fqdn;
$preview->save();
$this->dispatch('success', 'Domain generated.');
}
public function add(int $pull_request_id, string|null $pull_request_html_url = null)
{
try {
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found && !is_null($pull_request_html_url)) {
ApplicationPreview::create([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url
]);
}
$this->application->generate_preview_fqdn($pull_request_id);
$this->application->refresh();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function deploy(int $pull_request_id, string|null $pull_request_html_url = null)
{
try {
@ -71,6 +140,25 @@ protected function setDeploymentUuid()
}
public function stop(int $pull_request_id)
{
try {
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
} else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
}
}
GetContainersStatus::dispatchSync($this->application->destination->server);
$this->application->refresh();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function delete(int $pull_request_id)
{
try {
if ($this->application->destination->server->isSwarm()) {

View File

@ -43,6 +43,11 @@ public function mount()
$this->showTimeStamps = $this->resource->is_include_timestamps;
}
}
if ($this->resource?->getMorphClass() === 'App\Models\Application') {
if (str($this->container)->contains('-pr-')) {
$this->pull_request = "Pull Request: " . str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
}
}
}
}
public function doSomethingWithThisChunkOfOutput($output)
@ -77,13 +82,6 @@ public function getLogs($refresh = false)
if (!$this->server->isFunctional()) {
return;
}
if ($this->resource?->getMorphClass() === 'App\Models\Application') {
if (str($this->container)->contains('-pr-')) {
$this->pull_request = "Pull Request: " . str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
} else {
$this->pull_request = 'branch';
}
}
if (!$refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) return;
if (!$this->numberOfLines) {
$this->numberOfLines = 1000;

View File

@ -103,6 +103,14 @@ public function mount()
}
}
$this->containers = $this->containers->sort();
if (data_get($this->query,'pull_request_id')) {
$this->containers = $this->containers->filter(function ($container) {
return str_contains($container, $this->query['pull_request_id']);
});
ray($this->containers);
}
$this->loadMetrics();
} catch (\Exception $e) {
return handleError($e, $this);

View File

@ -70,9 +70,8 @@ public function submit()
$this->validate();
if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) {
ray('asdf');
if (!validate_dns_entry($this->settings->fqdn, $this->server)) {
$this->dispatch('error', "Validating DNS ({$this->settings->fqdn}) failed.<br><br>Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$this->dispatch('error', "Validating DNS failed.<br><br>Make sure you have added the DNS records correctly.<br><br>{$this->settings->fqdn}->{$this->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$error_show = true;
}
}

View File

@ -1052,4 +1052,29 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false
}
}
}
function generate_preview_fqdn(int $pull_request_id) {
$preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->id, $pull_request_id);
if (is_null(data_get($preview, 'fqdn')) && $this->fqdn) {
if (str($this->fqdn)->contains(',')) {
$url = Url::fromString(str($this->fqdn)->explode(',')[0]);
$preview_fqdn = getFqdnWithoutPort(str($this->fqdn)->explode(',')[0]);
} else {
$url = Url::fromString($this->fqdn);
if (data_get($preview, 'fqdn')) {
$preview_fqdn = getFqdnWithoutPort(data_get($preview, 'fqdn'));
}
}
$template = $this->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
$random = new Cuid2(7);
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn);
$preview_fqdn = "$schema://$preview_fqdn";
$preview->fqdn = $preview_fqdn;
$preview->save();
}
return $preview;
}
}

View File

@ -465,13 +465,32 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$appUuid = $appUuid . '-pr-' . $pull_request_id;
}
$labels = collect([]);
if ($application->fqdn) {
if ($pull_request_id !== 0) {
$domains = Str::of(data_get($preview, 'fqdn'))->explode(',');
} else {
if ($pull_request_id === 0) {
if ($application->fqdn) {
$domains = Str::of(data_get($application, 'fqdn'))->explode(',');
$labels = $labels->merge(fqdnLabelsForTraefik(
uuid: $appUuid,
domains: $domains,
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled()
));
// Add Caddy labels
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
uuid: $appUuid,
domains: $domains,
onlyPort: $onlyPort,
is_force_https_enabled: $application->isForceHttpsEnabled(),
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled()
));
}
} else {
if ($preview->fqdn) {
$domains = Str::of(data_get($preview, 'fqdn'))->explode(',');
}
// Add Traefik labels
$labels = $labels->merge(fqdnLabelsForTraefik(
uuid: $appUuid,
domains: $domains,
@ -490,6 +509,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_gzip_enabled: $application->isGzipEnabled(),
is_stripprefix_enabled: $application->isStripprefixEnabled()
));
}
return $labels->all();
}

View File

@ -1,54 +1,68 @@
@props([
'title' => 'Are you sure?',
'buttonTitle' => 'Open Modal',
'isErrorButton' => false,
'buttonTitle' => 'REWRITE THIS BUTTON TITLE PLEASSSSEEEE',
'buttonFullWidth' => false,
'customButton' => null,
'disabled' => false,
'action' => 'delete',
'content' => null,
])
<div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto">
@if ($content)
<div @click="modalOpen=true">
{{ $content }}
</div>
@else
@if ($disabled)
@if ($buttonFullWidth)
<x-forms.button class="w-full" isError disabled wire:target>
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button isError disabled wire:target>
{{ $buttonTitle }}
</x-forms.button>
@endif
@elseif ($isErrorButton)
@if ($buttonFullWidth)
<x-forms.button class="w-full" isError @click="modalOpen=true">
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button isError @click="modalOpen=true">
{{ $buttonTitle }}
</x-forms.button>
@endif
@if ($customButton)
@if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="w-full">
{{ $customButton }}
</x-forms.button>
@else
@if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="flex w-full gap-2" wire:target>
{{ $buttonTitle }}
</x-forms.button>
<x-forms.button @click="modalOpen=true">
{{ $customButton }}
</x-forms.button>
@endif
@else
@if ($content)
<div @click="modalOpen=true">
{{ $content }}
</div>
@else
@if ($disabled)
@if ($buttonFullWidth)
<x-forms.button class="w-full" isError disabled wire:target>
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button isError disabled wire:target>
{{ $buttonTitle }}
</x-forms.button>
@endif
@elseif ($isErrorButton)
@if ($buttonFullWidth)
<x-forms.button class="w-full" isError @click="modalOpen=true">
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button isError @click="modalOpen=true">
{{ $buttonTitle }}
</x-forms.button>
@endif
@else
<x-forms.button @click="modalOpen=true" class="flex gap-2" wire:target>
{{ $buttonTitle }}
</x-forms.button>
@if ($buttonFullWidth)
<x-forms.button @click="modalOpen=true" class="flex w-full gap-2" wire:target>
{{ $buttonTitle }}
</x-forms.button>
@else
<x-forms.button @click="modalOpen=true" class="flex gap-2" wire:target>
{{ $buttonTitle }}
</x-forms.button>
@endif
@endif
@endif
@endif
<template x-teleport="body">
<div x-show="modalOpen"
class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen" style="zoom:1.1;" x-cloak>
class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen"
style="zoom:1.1;" x-cloak>
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="modalOpen=false"

View File

@ -8,6 +8,8 @@
<div class="flex flex-col gap-2 pb-4">
<x-forms.input id="application.preview_url_template" label="Preview URL Template"
helper="Templates:<span class='text-helper'>@@{{ random }}</span> to generate random sub-domain each time a PR is deployed, <span class='text-helper'>@@{{ pr_id }}</span> to use pull request ID as sub-domain or <span class='text-helper'>@@{{ domain }}</span> to replace the domain name with the application's domain name." />
<div class="">Domain Preview: {{ $preview_url_template }}</div>
@if ($preview_url_template)
<div class="">Domain Preview: {{ $preview_url_template }}</div>
@endif
</div>
</form>

View File

@ -39,7 +39,11 @@ class="dark:text-warning">{{ $application->destination->server->name }}</span>.<
<x-external-link />
</a>
</td>
<td>
<td class="flex flex-col gap-1 md:flex-row">
<x-forms.button
wire:click="add('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Add
</x-forms.button>
<x-forms.button
wire:click="deploy('{{ data_get($pull_request, 'number') }}', '{{ data_get($pull_request, 'html_url') }}')">
Deploy
@ -55,9 +59,9 @@ class="dark:text-warning">{{ $application->destination->server->name }}</span>.<
</div>
@if ($application->previews->count() > 0)
<div class="pb-4">Previews</div>
<div class="flex flex-wrap gap-6">
@foreach ($application->previews as $preview)
<div class="flex flex-col p-4 dark:bg-coolgray-100">
<div class="flex flex-wrap w-full gap-4">
@foreach (data_get($application, 'previews') as $previewName => $preview)
<div class="flex flex-col w-full p-4 border dark:border-coolgray-200">
<div class="flex gap-2">PR #{{ data_get($preview, 'pull_request_id') }} |
@if (Str::of(data_get($preview, 'status'))->startsWith('running'))
<x-status.running :status="data_get($preview, 'status')" />
@ -77,9 +81,15 @@ class="dark:text-warning">{{ $application->destination->server->name }}</span>.<
<x-external-link />
</a>
</div>
<form wire:submit="save_preview('{{ $preview->id }}')" class="flex items-end gap-2 pt-4">
<x-forms.input label="Domain" helper="One domain per preview."
id="application.previews.{{ $previewName }}.fqdn"></x-forms.input>
<x-forms.button type="submit">Save</x-forms.button>
<x-forms.button wire:click="generate_preview('{{ $preview->id }}')">Generate
Domain</x-forms.button>
</form>
<div class="flex items-center gap-2 pt-6">
<x-forms.button class="dark:bg-coolgray-500"
wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
<x-forms.button wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
@if (data_get($preview, 'status') === 'exited')
Deploy
@else
@ -89,20 +99,43 @@ class="dark:text-warning">{{ $application->destination->server->name }}</span>.<
@if (count($parameters) > 0)
<a
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
<x-forms.button class="dark:bg-coolgray-500">
<x-forms.button>
Deployment Logs
</x-forms.button>
</a>
<a
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
<x-forms.button class="dark:bg-coolgray-500">
<x-forms.button>
Application Logs
</x-forms.button>
</a>
@endif
<x-forms.button isError class="dark:bg-coolgray-500"
wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Delete
</x-forms.button>
<div class="flex-1"></div>
@if (data_get($preview, 'status') !== 'exited')
<x-modal-confirmation isErrorButton
action="stop({{ data_get($preview, 'pull_request_id') }})">
<x-slot:customButton>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
<path
d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z">
</path>
</svg>
Stop
</x-slot:customButton>
This will stop the preview deployment. <br>Please think again.
</x-modal-confirmation>
@endif
<x-modal-confirmation isErrorButton
action="delete({{ data_get($preview, 'pull_request_id') }})" buttonTitle="Delete">
This will delete the preview deployment. <br>Please think again.
</x-modal-confirmation>
</div>
</div>
@endforeach

View File

@ -31,7 +31,7 @@ class="relative flex flex-col bg-white border cursor-default dark:text-white box
</div>
@endif
</div>
@if ($resource?->additional_networks?->count() > 0)
@if ($resource?->additional_networks?->count() > 0 && data_get($resource, 'build_pack') !== 'dockercompose')
<h3>Additional Server(s)</h3>
@foreach ($resource->additional_networks as $destination)
<div class="flex flex-col gap-2">
@ -73,7 +73,7 @@ class="absolute bg-error -top-1 -left-1 badge "></div>
@endforeach
@endif
</div>
@if ($resource->getMorphClass() === 'App\Models\Application')
@if ($resource->getMorphClass() === 'App\Models\Application' && data_get($resource, 'build_pack') !== 'dockercompose')
@if (count($networks) > 0)
<h4>Choose another server</h4>
<div class="pb-4 description">(experimental) </div>

View File

@ -1,4 +1,4 @@
<div>
<div class="p-4 my-4 border dark:border-coolgray-200">
<div x-init="$wire.getLogs" id="screen" x-data="{
fullscreen: false,
alwaysScroll: false,
@ -33,13 +33,12 @@
screen.scrollTop = 0;
}
}">
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 ">
@if ($resource?->type() === 'application')
<h3>{{ $container }}</h3>
<h4>{{ $container }}</h4>
@else
<h3>{{ str($container)->beforeLast('-')->headline() }}</h3>
@endif
<div>Server: {{ $server->name }} </div>
@if ($pull_request)
<div>({{ $pull_request }})</div>
@endif

View File

@ -4,18 +4,21 @@
<h1>Logs</h1>
<livewire:project.application.heading :application="$resource" />
<div class="pt-4">
<h2 class="pb-4">Logs</h2>
<h2>Logs</h2>
<div class="subtitle">Here you can see the logs of the application.</div>
<div class="pt-2" wire:loading wire:target="loadContainers">
Loading containers...
</div>
@forelse ($servers as $server)
<h3 x-init="$wire.loadContainers({{ $server->id }})"></h3>
<div wire:loading.remove wire:target="loadContainers">
@forelse (data_get($server,'containers',[]) as $container)
<livewire:project.shared.get-logs :server="$server" :resource="$resource" :container="data_get($container, 'Names')" />
@empty
<div class="pt-2">No containers are not running on server: {{ $server->name }}</div>
@endforelse
<div class="py-2">
<h2 wire:loading.remove x-init="$wire.loadContainers({{ $server->id }})">Server: {{ $server->name }}</h2>
<div wire:loading.remove wire:target="loadContainers">
@forelse (data_get($server,'containers',[]) as $container)
<livewire:project.shared.get-logs :server="$server" :resource="$resource" :container="data_get($container, 'Names')" />
@empty
<div class="pt-2">No containers are not running on server: {{ $server->name }}</div>
@endforelse
</div>
</div>
@empty
<div>No functional server found for the application.</div>