diff --git a/app/Models/Server.php b/app/Models/Server.php index 6b59654ef..06426f211 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -155,12 +155,7 @@ protected static function booted() 'server_id' => $server->id, ])->save(); } else { - (new StandaloneDocker)->forceFill([ - 'id' => 0, - 'name' => 'coolify', - 'network' => 'coolify', - 'server_id' => $server->id, - ])->saveQuietly(); + (new StandaloneDocker)->forceFill($server->defaultStandaloneDockerAttributes(id: 0))->saveQuietly(); } } else { if ($server->isSwarm()) { @@ -171,12 +166,7 @@ protected static function booted() ]); } else { $standaloneDocker = new StandaloneDocker; - $standaloneDocker->forceFill([ - 'name' => 'coolify', - 'uuid' => (string) new Cuid2, - 'network' => 'coolify', - 'server_id' => $server->id, - ]); + $standaloneDocker->forceFill($server->defaultStandaloneDockerAttributes()); $standaloneDocker->saveQuietly(); } } @@ -1043,6 +1033,25 @@ public function team() return $this->belongsTo(Team::class); } + /** + * @return array{id?: int, name: string, uuid: string, network: string, server_id: int} + */ + public function defaultStandaloneDockerAttributes(?int $id = null): array + { + $attributes = [ + 'name' => 'coolify', + 'uuid' => (string) new Cuid2, + 'network' => 'coolify', + 'server_id' => $this->id, + ]; + + if (! is_null($id)) { + $attributes['id'] = $id; + } + + return $attributes; + } + public function environment_variables() { return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'server'); diff --git a/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php b/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php index 0207ed955..a6a6fe872 100644 --- a/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php +++ b/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php @@ -5,17 +5,23 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ /** * Run the migrations. */ public function up(): void { - DB::statement("ALTER TABLE shared_environment_variables DROP CONSTRAINT shared_environment_variables_type_check"); - DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment', 'server'))"); - Schema::table('shared_environment_variables', function (Blueprint $table) { - $table->foreignId('server_id')->nullable()->constrained()->onDelete('cascade'); - $table->unique(['key', 'server_id', 'team_id']); + DB::transaction(function () { + DB::statement('ALTER TABLE shared_environment_variables DROP CONSTRAINT IF EXISTS shared_environment_variables_type_check'); + DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment', 'server'))"); + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->foreignId('server_id')->nullable()->constrained()->onDelete('cascade'); + // NULL != NULL in PostgreSQL unique indexes, so this only enforces uniqueness + // for server-scoped rows (where server_id is non-null). Other scopes are covered + // by existing unique constraints on ['key', 'project_id', 'team_id'] and ['key', 'environment_id', 'team_id']. + $table->unique(['key', 'server_id', 'team_id']); + }); }); } @@ -24,12 +30,14 @@ public function up(): void */ public function down(): void { - Schema::table('shared_environment_variables', function (Blueprint $table) { - $table->dropUnique(['key', 'server_id', 'team_id']); - $table->dropForeign(['server_id']); - $table->dropColumn('server_id'); + DB::transaction(function () { + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->dropUnique(['key', 'server_id', 'team_id']); + $table->dropForeign(['server_id']); + $table->dropColumn('server_id'); + }); + DB::statement('ALTER TABLE shared_environment_variables DROP CONSTRAINT IF EXISTS shared_environment_variables_type_check'); + DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment'))"); }); - DB::statement("ALTER TABLE shared_environment_variables DROP CONSTRAINT shared_environment_variables_type_check"); - DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment'))"); } }; diff --git a/tests/Unit/ServerBootstrapDestinationAttributesTest.php b/tests/Unit/ServerBootstrapDestinationAttributesTest.php new file mode 100644 index 000000000..e9d229fc2 --- /dev/null +++ b/tests/Unit/ServerBootstrapDestinationAttributesTest.php @@ -0,0 +1,20 @@ +id = 0; + + $attributes = $server->defaultStandaloneDockerAttributes(id: 0); + + expect($attributes) + ->toMatchArray([ + 'id' => 0, + 'name' => 'coolify', + 'network' => 'coolify', + 'server_id' => 0, + ]) + ->and($attributes['uuid'])->toBeString() + ->and($attributes['uuid'])->not->toBe(''); +});