Merge pull request #5573 from coollabsio/next

v4.0.0-beta.407
This commit is contained in:
Andras Bacsai
2025-04-09 09:21:34 +02:00
committed by GitHub
13 changed files with 314 additions and 43 deletions

View File

@@ -329,7 +329,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else {
$this->write_deployment_configurations();
}
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
$this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}");
$this->graceful_shutdown_container($this->deployment_uuid);
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
@@ -1361,7 +1361,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
$this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage.");
$this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}");
$this->graceful_shutdown_container($this->deployment_uuid);
$this->execute_remote_command(
[
@@ -1710,6 +1709,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
]);
$this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo'));
}
$custom_network_aliases = [];
if (is_array($this->application->custom_network_aliases) && count($this->application->custom_network_aliases) > 0) {
$custom_network_aliases = $this->application->custom_network_aliases;
}
$docker_compose = [
'services' => [
$this->container_name => [
@@ -1719,9 +1722,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'expose' => $ports,
'networks' => [
$this->destination->network => [
'aliases' => [
$this->container_name,
],
'aliases' => array_merge(
[$this->container_name],
$custom_network_aliases
),
],
],
'mem_limit' => $this->application->limits_memory,

View File

@@ -68,6 +68,7 @@ class General extends Component
'application.publish_directory' => 'nullable',
'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable',
'application.custom_network_aliases' => 'nullable',
'application.dockerfile' => 'nullable',
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
@@ -121,6 +122,7 @@ class General extends Component
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.custom_docker_run_options' => 'Custom docker run commands',
'application.custom_network_aliases' => 'Custom docker network aliases',
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.custom_nginx_configuration' => 'Custom Nginx configuration',

View File

@@ -21,7 +21,7 @@ class General extends Component
public string $redis_username;
public string $redis_password;
public ?string $redis_password;
public string $redis_version;

View File

@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class All extends Component
{
use EnvironmentVariableProtection;
public $resource;
public string $resourceClass;
@@ -138,18 +141,58 @@ class All extends Component
private function handleBulkSubmit()
{
$variables = parseEnvFormatToArray($this->variables);
$changesMade = false;
$errorOccurred = false;
$this->deleteRemovedVariables(false, $variables);
$this->updateOrCreateVariables(false, $variables);
// Try to delete removed variables
$deletedCount = $this->deleteRemovedVariables(false, $variables);
if ($deletedCount > 0) {
$changesMade = true;
} elseif ($deletedCount === 0 && $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->exists()) {
// If we tried to delete but couldn't (due to Docker Compose), mark as error
$errorOccurred = true;
}
// Update or create variables
$updatedCount = $this->updateOrCreateVariables(false, $variables);
if ($updatedCount > 0) {
$changesMade = true;
}
if ($this->showPreview) {
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
$this->deleteRemovedVariables(true, $previewVariables);
$this->updateOrCreateVariables(true, $previewVariables);
// Try to delete removed preview variables
$deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables);
if ($deletedPreviewCount > 0) {
$changesMade = true;
} elseif ($deletedPreviewCount === 0 && $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($previewVariables))->exists()) {
// If we tried to delete but couldn't (due to Docker Compose), mark as error
$errorOccurred = true;
}
// Update or create preview variables
$updatedPreviewCount = $this->updateOrCreateVariables(true, $previewVariables);
if ($updatedPreviewCount > 0) {
$changesMade = true;
}
}
// Debug information
\Log::info('Environment variables update status', [
'deletedCount' => $deletedCount,
'updatedCount' => $updatedCount,
'deletedPreviewCount' => $deletedPreviewCount ?? 0,
'updatedPreviewCount' => $updatedPreviewCount ?? 0,
'changesMade' => $changesMade,
'errorOccurred' => $errorOccurred,
]);
// Only show success message if changes were actually made and no errors occurred
if ($changesMade && ! $errorOccurred) {
$this->dispatch('success', 'Environment variables updated.');
}
}
private function handleSingleSubmit($data)
{
@@ -184,11 +227,46 @@ class All extends Component
private function deleteRemovedVariables($isPreview, $variables)
{
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
// Get all environment variables that will be deleted
$variablesToDelete = $this->resource->$method()->whereNotIn('key', array_keys($variables))->get();
// If there are no variables to delete, return 0
if ($variablesToDelete->isEmpty()) {
return 0;
}
// Check for system variables that shouldn't be deleted
foreach ($variablesToDelete as $envVar) {
if ($this->isProtectedEnvironmentVariable($envVar->key)) {
$this->dispatch('error', "Cannot delete system environment variable '{$envVar->key}'.");
return 0;
}
}
// Check if any of these variables are used in Docker Compose
if ($this->resource->type() === 'service' || $this->resource->build_pack === 'dockercompose') {
foreach ($variablesToDelete as $envVar) {
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($envVar->key, $this->resource->docker_compose);
if ($isUsed) {
$this->dispatch('error', "Cannot delete environment variable '{$envVar->key}' <br><br>Please remove it from the Docker Compose file first.");
return 0;
}
}
}
// If we get here, no variables are used in Docker Compose, so we can delete them
$this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
return $variablesToDelete->count();
}
private function updateOrCreateVariables($isPreview, $variables)
{
$count = 0;
foreach ($variables as $key => $value) {
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
continue;
@@ -198,8 +276,12 @@ class All extends Component
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
// Only count as a change if the value actually changed
if ($found->value !== $value) {
$found->value = $value;
$found->save();
$count++;
}
}
} else {
$environment = new EnvironmentVariable;
@@ -212,8 +294,11 @@ class All extends Component
$environment->resourceable_type = $this->resource->getMorphClass();
$environment->save();
$count++;
}
}
return $count;
}
public function refreshEnvs()

View File

@@ -4,10 +4,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use App\Models\SharedEnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class Show extends Component
{
use EnvironmentVariableProtection;
public $parameters;
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
@@ -40,6 +43,8 @@ class Show extends Component
public bool $is_really_required = false;
public bool $is_redis_credential = false;
protected $listeners = [
'refreshEnvs' => 'refresh',
'refresh',
@@ -65,7 +70,9 @@ class Show extends Component
}
$this->parameters = get_route_parameters();
$this->checkEnvs();
if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) {
$this->is_redis_credential = true;
}
}
public function refresh()
@@ -171,6 +178,24 @@ class Show extends Component
public function delete()
{
try {
// Check if the variable is protected
if ($this->isProtectedEnvironmentVariable($this->env->key)) {
$this->dispatch('error', "Cannot delete system environment variable '{$this->env->key}'.");
return;
}
// Check if the variable is used in Docker Compose
if ($this->type === 'service' || $this->type === 'application' && $this->env->resource()?->docker_compose) {
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resource()?->docker_compose);
if ($isUsed) {
$this->dispatch('error', "Cannot delete environment variable '{$this->env->key}' <br><br>Please remove it from the Docker Compose file first.");
return;
}
}
$this->env->delete();
$this->dispatch('environmentVariableDeleted');
$this->dispatch('success', 'Environment variable deleted successfully.');

View File

@@ -45,6 +45,7 @@ use Visus\Cuid2\Cuid2;
'start_command' => ['type' => 'string', 'description' => 'Start command.'],
'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'],
'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'],
'custom_network_aliases' => ['type' => 'string', 'nullable' => true, 'description' => 'Network aliases for Docker container.'],
'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'],
'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'],
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
@@ -115,6 +116,68 @@ class Application extends BaseModel
protected $appends = ['server_status'];
protected $casts = ['custom_network_aliases' => 'array'];
public function customNetworkAliases(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return null;
}
// If it's already a JSON string, decode it
if (is_string($value) && $this->isJson($value)) {
$value = json_decode($value, true);
}
// If it's a string but not JSON, treat it as a comma-separated list
if (is_string($value) && ! is_array($value)) {
$value = explode(',', $value);
}
$value = collect($value)
->map(function ($alias) {
if (is_string($alias)) {
return str_replace(' ', '-', trim($alias));
}
return null;
})
->filter()
->unique() // Remove duplicate values
->values()
->toArray();
return empty($value) ? null : json_encode($value);
},
get: function ($value) {
if (is_null($value)) {
return null;
}
if (is_string($value) && $this->isJson($value)) {
return json_decode($value, true);
}
return is_array($value) ? $value : [];
}
);
}
/**
* Check if a string is a valid JSON
*/
private function isJson($string)
{
if (! is_string($string)) {
return false;
}
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
protected static function booted()
{
static::addGlobalScope('withRelations', function ($builder) {

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Traits;
use Symfony\Component\Yaml\Yaml;
trait EnvironmentVariableProtection
{
/**
* Check if an environment variable is protected from deletion
*
* @param string $key The environment variable key to check
* @return bool True if the variable is protected, false otherwise
*/
protected function isProtectedEnvironmentVariable(string $key): bool
{
return str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL');
}
/**
* Check if an environment variable is used in Docker Compose
*
* @param string $key The environment variable key to check
* @param string|null $dockerCompose The Docker Compose YAML content
* @return array [bool $isUsed, string $reason] Whether the variable is used and the reason if it is
*/
protected function isEnvironmentVariableUsedInDockerCompose(string $key, ?string $dockerCompose): array
{
if (empty($dockerCompose)) {
return [false, ''];
}
try {
$dockerComposeData = Yaml::parse($dockerCompose);
$dockerEnvVars = data_get($dockerComposeData, 'services.*.environment');
foreach ($dockerEnvVars as $serviceEnvs) {
if (! is_array($serviceEnvs)) {
continue;
}
// Check for direct variable usage
foreach ($serviceEnvs as $env => $value) {
if ($env === $key) {
return [true, "Environment variable '{$key}' is used directly in the Docker Compose file."];
}
}
// Check for variable references in values
foreach ($serviceEnvs as $env => $value) {
if (is_string($value) && str_contains($value, '$'.$key)) {
return [true, "Environment variable '{$key}' is referenced in the Docker Compose file."];
}
}
}
} catch (\Exception $e) {
// If there's an error parsing the Docker Compose file, we'll assume it's not used
return [false, ''];
}
return [false, ''];
}
}

View File

@@ -2,7 +2,7 @@
return [
'coolify' => [
'version' => '4.0.0-beta.406',
'version' => '4.0.0-beta.407',
'helper_version' => '1.0.8',
'realtime_version' => '1.0.6',
'self_hosted' => env('SELF_HOSTED', true),

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('applications', function (Blueprint $table) {
$table->text('custom_network_aliases')->nullable();
});
}
public function down()
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('custom_network_aliases');
});
}
};

View File

@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.406"
"version": "4.0.0-beta.407"
},
"nightly": {
"version": "4.0.0-beta.407"
"version": "4.0.0-beta.408"
},
"helper": {
"version": "1.0.8"

View File

@@ -342,6 +342,11 @@
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
@endif
@if (!$application->destination->server->isSwarm())
<x-forms.input id="application.custom_network_aliases" label="Network Aliases"
helper="A comma separated list of custom network aliases you would like to add for container in Docker network.<br><br><span class='inline-block font-bold dark:text-warning'>Example:</span><br>api.internal,api.local"
wire:model="application.custom_network_aliases" />
@endif
</div>
@if ($application->settings->is_container_label_readonly_enabled)

View File

@@ -31,18 +31,19 @@
@else
<div class="flex flex-col w-full gap-2 lg:flex-row">
@if ($is_multiline)
<x-forms.input isMultiline="{{ $is_multiline }}" id="key" />
<x-forms.textarea type="password" id="value" />
<x-forms.input :required="$is_redis_credential" isMultiline="{{ $is_multiline }}" id="key" />
<x-forms.textarea :required="$is_redis_credential" type="password" id="value" />
@else
<x-forms.input id="key" />
<x-forms.input type="password" id="value" />
<x-forms.input :disabled="$is_redis_credential" :required="$is_redis_credential" id="key" />
<x-forms.input :required="$is_redis_credential" type="password" id="value" />
@endif
@if ($is_shared)
<x-forms.input disabled type="password" id="real_value" />
<x-forms.input :disabled="$is_redis_credential" :required="$is_redis_credential" disabled type="password" id="real_value" />
@endif
</div>
@endif
<div class="flex flex-col w-full gap-2 lg:flex-row">
@if (!$is_redis_credential)
@if ($type === 'service')
<x-forms.checkbox instantSave id="is_build_time"
helper="If you are using Docker, remember to modify the file to be ready to receive the build time args. Ex.: for docker file, add `ARG name_of_the_variable`, or dockercompose add `- 'name_of_the_variable=${name_of_the_variable}'`"
@@ -75,6 +76,7 @@
@endif
@endif
@endif
@endif
<div class="flex-1"></div>
@if ($isDisabled)
<x-forms.button disabled type="submit">

View File

@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.406"
"version": "4.0.0-beta.407"
},
"nightly": {
"version": "4.0.0-beta.407"
"version": "4.0.0-beta.408"
},
"helper": {
"version": "1.0.8"