feat(event): introduce ApplicationConfigurationChanged event to handle team-specific configuration updates and broadcast changes

feat(envs): Generate hash from secrets to invalidate docker layers
This commit is contained in:
Andras Bacsai
2025-09-22 09:44:30 +02:00
parent 3cc2426b9a
commit 4f71d14d39
3 changed files with 104 additions and 5 deletions

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ApplicationConfigurationChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public ?int $teamId = null;
public function __construct($teamId = null)
{
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id;
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
if (is_null($this->teamId)) {
return [];
}
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Jobs;
use App\Actions\Docker\GetContainersStatus;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProcessStatus;
use App\Events\ApplicationConfigurationChanged;
use App\Events\ServiceStatusChanged;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
@@ -147,6 +148,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private Collection $saved_outputs;
private ?string $secrets_hash_key = null;
private ?string $full_healthcheck_url = null;
private string $serverUser = 'root';
@@ -2712,22 +2715,28 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if ($this->application->build_pack === 'nixpacks') {
$variables = collect($this->nixpacks_plan_json->get('variables'));
} else {
// Generate environment variables for build process (filters by is_buildtime = true)
$this->generate_env_variables();
$variables = collect([])->merge($this->env_args);
}
// Check if build secrets are enabled and BuildKit is supported
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
$this->generate_build_secrets($variables);
$this->build_args = '';
} else {
// Fall back to traditional build args
$secrets_hash = '';
if ($variables->isNotEmpty()) {
$secrets_hash = $this->generate_secrets_hash($variables);
}
$this->build_args = $variables->map(function ($value, $key) {
$value = escapeshellarg($value);
return "--build-arg {$key}={$value}";
});
if ($secrets_hash) {
$this->build_args->push("--build-arg COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}");
}
}
}
@@ -2746,13 +2755,18 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
return '';
}
return $variables
$secrets_hash = $this->generate_secrets_hash($variables);
$env_flags = $variables
->map(function ($env) {
$escaped_value = escapeshellarg($env->real_value);
return "-e {$env->key}={$escaped_value}";
})
->implode(' ');
$env_flags .= " -e COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}";
return $env_flags;
}
private function generate_build_secrets(Collection $variables)
@@ -2768,6 +2782,36 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
return "--secret id={$key},env={$key}";
})
->implode(' ');
$this->build_secrets .= ' --secret id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH';
}
private function generate_secrets_hash($variables)
{
if (! $this->secrets_hash_key) {
$this->secrets_hash_key = bin2hex(random_bytes(32));
}
if ($variables instanceof Collection) {
$secrets_string = $variables
->mapWithKeys(function ($value, $key) {
return [$key => $value];
})
->sortKeys()
->map(function ($value, $key) {
return "{$key}={$value}";
})
->implode('|');
} else {
$secrets_string = $variables
->map(function ($env) {
return "{$env->key}={$env->real_value}";
})
->sort()
->implode('|');
}
return hash_hmac('sha256', $secrets_string, $this->secrets_hash_key);
}
private function add_build_env_variables_to_dockerfile()
@@ -2809,6 +2853,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
}
}
if ($envs->isNotEmpty()) {
$secrets_hash = $this->generate_secrets_hash($envs);
$dockerfile->splice(1, 0, ["ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"]);
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"),
@@ -2850,6 +2900,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
// Generate mount strings for all secrets
$mountStrings = $variables->map(fn ($env) => "--mount=type=secret,id={$env->key},env={$env->key}")->implode(' ');
// Add mount for the secrets hash to ensure cache invalidation
$mountStrings .= ' --mount=type=secret,id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH';
$modified = false;
$dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) {
$trimmed = ltrim($line);
@@ -3186,6 +3239,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
queue_next_deployment($this->application);
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
ray($this->application->team()->id);
event(new ApplicationConfigurationChanged($this->application->team()->id));
if (! $this->only_this_server) {
$this->deploy_to_additional_destinations();
}

View File

@@ -20,7 +20,15 @@ class ConfigurationChecker extends Component
public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
protected $listeners = ['configurationChanged'];
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationConfigurationChanged" => 'configurationChanged',
'configurationChanged' => 'configurationChanged',
];
}
public function mount()
{