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:
35
app/Events/ApplicationConfigurationChanged.php
Normal file
35
app/Events/ApplicationConfigurationChanged.php
Normal 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}"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,7 @@ namespace App\Jobs;
|
|||||||
use App\Actions\Docker\GetContainersStatus;
|
use App\Actions\Docker\GetContainersStatus;
|
||||||
use App\Enums\ApplicationDeploymentStatus;
|
use App\Enums\ApplicationDeploymentStatus;
|
||||||
use App\Enums\ProcessStatus;
|
use App\Enums\ProcessStatus;
|
||||||
|
use App\Events\ApplicationConfigurationChanged;
|
||||||
use App\Events\ServiceStatusChanged;
|
use App\Events\ServiceStatusChanged;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationDeploymentQueue;
|
use App\Models\ApplicationDeploymentQueue;
|
||||||
@@ -147,6 +148,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
private Collection $saved_outputs;
|
private Collection $saved_outputs;
|
||||||
|
|
||||||
|
private ?string $secrets_hash_key = null;
|
||||||
|
|
||||||
private ?string $full_healthcheck_url = null;
|
private ?string $full_healthcheck_url = null;
|
||||||
|
|
||||||
private string $serverUser = 'root';
|
private string $serverUser = 'root';
|
||||||
@@ -2712,22 +2715,28 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
if ($this->application->build_pack === 'nixpacks') {
|
if ($this->application->build_pack === 'nixpacks') {
|
||||||
$variables = collect($this->nixpacks_plan_json->get('variables'));
|
$variables = collect($this->nixpacks_plan_json->get('variables'));
|
||||||
} else {
|
} else {
|
||||||
// Generate environment variables for build process (filters by is_buildtime = true)
|
|
||||||
$this->generate_env_variables();
|
$this->generate_env_variables();
|
||||||
$variables = collect([])->merge($this->env_args);
|
$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) {
|
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||||
$this->generate_build_secrets($variables);
|
$this->generate_build_secrets($variables);
|
||||||
$this->build_args = '';
|
$this->build_args = '';
|
||||||
} else {
|
} 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) {
|
$this->build_args = $variables->map(function ($value, $key) {
|
||||||
$value = escapeshellarg($value);
|
$value = escapeshellarg($value);
|
||||||
|
|
||||||
return "--build-arg {$key}={$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 '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $variables
|
$secrets_hash = $this->generate_secrets_hash($variables);
|
||||||
|
$env_flags = $variables
|
||||||
->map(function ($env) {
|
->map(function ($env) {
|
||||||
$escaped_value = escapeshellarg($env->real_value);
|
$escaped_value = escapeshellarg($env->real_value);
|
||||||
|
|
||||||
return "-e {$env->key}={$escaped_value}";
|
return "-e {$env->key}={$escaped_value}";
|
||||||
})
|
})
|
||||||
->implode(' ');
|
->implode(' ');
|
||||||
|
|
||||||
|
$env_flags .= " -e COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}";
|
||||||
|
|
||||||
|
return $env_flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generate_build_secrets(Collection $variables)
|
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}";
|
return "--secret id={$key},env={$key}";
|
||||||
})
|
})
|
||||||
->implode(' ');
|
->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()
|
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"));
|
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
|
||||||
$this->execute_remote_command([
|
$this->execute_remote_command([
|
||||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"),
|
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
|
// Generate mount strings for all secrets
|
||||||
$mountStrings = $variables->map(fn ($env) => "--mount=type=secret,id={$env->key},env={$env->key}")->implode(' ');
|
$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;
|
$modified = false;
|
||||||
$dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) {
|
$dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) {
|
||||||
$trimmed = ltrim($line);
|
$trimmed = ltrim($line);
|
||||||
@@ -3186,6 +3239,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
queue_next_deployment($this->application);
|
queue_next_deployment($this->application);
|
||||||
|
|
||||||
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||||
|
ray($this->application->team()->id);
|
||||||
|
event(new ApplicationConfigurationChanged($this->application->team()->id));
|
||||||
|
|
||||||
if (! $this->only_this_server) {
|
if (! $this->only_this_server) {
|
||||||
$this->deploy_to_additional_destinations();
|
$this->deploy_to_additional_destinations();
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,15 @@ class ConfigurationChecker extends Component
|
|||||||
|
|
||||||
public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
|
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()
|
public function mount()
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user