feat(server): add configurable SSH connection timeout per server (#9844)
This commit is contained in:
commit
092ea3bb7f
11 changed files with 196 additions and 10 deletions
|
|
@ -71,7 +71,7 @@ public static function establishNewMultiplexedConnection(Server $server): bool
|
||||||
$sshConfig = self::serverSshConfiguration($server);
|
$sshConfig = self::serverSshConfiguration($server);
|
||||||
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||||
$muxSocket = $sshConfig['muxFilename'];
|
$muxSocket = $sshConfig['muxFilename'];
|
||||||
$connectionTimeout = config('constants.ssh.connection_timeout');
|
$connectionTimeout = self::getConnectionTimeout($server);
|
||||||
$serverInterval = config('constants.ssh.server_interval');
|
$serverInterval = config('constants.ssh.server_interval');
|
||||||
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||||
|
|
||||||
|
|
@ -140,7 +140,7 @@ public static function generateScpCommand(Server $server, string $source, string
|
||||||
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||||
}
|
}
|
||||||
|
|
||||||
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
|
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, self::getConnectionTimeout($server), config('constants.ssh.server_interval'), isScp: true);
|
||||||
if ($server->isIpv6()) {
|
if ($server->isIpv6()) {
|
||||||
$scp_command .= "{$source} ".escapeshellarg($server->user).'@['.escapeshellarg($server->ip)."]:{$dest}";
|
$scp_command .= "{$source} ".escapeshellarg($server->user).'@['.escapeshellarg($server->ip)."]:{$dest}";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -184,7 +184,7 @@ public static function generateSshCommand(Server $server, string $command, bool
|
||||||
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
|
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
|
||||||
}
|
}
|
||||||
|
|
||||||
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
|
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, self::getConnectionTimeout($server), config('constants.ssh.server_interval'));
|
||||||
|
|
||||||
$delimiter = Hash::make($command);
|
$delimiter = Hash::make($command);
|
||||||
$delimiter = base64_encode($delimiter);
|
$delimiter = base64_encode($delimiter);
|
||||||
|
|
@ -243,6 +243,15 @@ private static function validateSshKey(PrivateKey $privateKey): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getConnectionTimeout(Server $server): int
|
||||||
|
{
|
||||||
|
$timeout = data_get($server, 'settings.connection_timeout');
|
||||||
|
|
||||||
|
return is_numeric($timeout) && (int) $timeout > 0
|
||||||
|
? (int) $timeout
|
||||||
|
: (int) config('constants.ssh.connection_timeout');
|
||||||
|
}
|
||||||
|
|
||||||
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
|
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
|
||||||
{
|
{
|
||||||
$options = "-i {$sshKeyLocation} "
|
$options = "-i {$sshKeyLocation} "
|
||||||
|
|
|
||||||
|
|
@ -612,6 +612,7 @@ public function create_server(Request $request)
|
||||||
'deployment_queue_limit' => ['type' => 'integer', 'description' => 'Maximum number of queued deployments.'],
|
'deployment_queue_limit' => ['type' => 'integer', 'description' => 'Maximum number of queued deployments.'],
|
||||||
'server_disk_usage_notification_threshold' => ['type' => 'integer', 'description' => 'Server disk usage notification threshold (%).'],
|
'server_disk_usage_notification_threshold' => ['type' => 'integer', 'description' => 'Server disk usage notification threshold (%).'],
|
||||||
'server_disk_usage_check_frequency' => ['type' => 'string', 'description' => 'Cron expression for disk usage check frequency.'],
|
'server_disk_usage_check_frequency' => ['type' => 'string', 'description' => 'Cron expression for disk usage check frequency.'],
|
||||||
|
'connection_timeout' => ['type' => 'integer', 'description' => 'SSH connection timeout in seconds (1-300). Default: 10.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -648,7 +649,7 @@ public function create_server(Request $request)
|
||||||
)]
|
)]
|
||||||
public function update_server(Request $request)
|
public function update_server(Request $request)
|
||||||
{
|
{
|
||||||
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency'];
|
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'connection_timeout'];
|
||||||
|
|
||||||
$teamId = getTeamIdFromToken();
|
$teamId = getTeamIdFromToken();
|
||||||
if (is_null($teamId)) {
|
if (is_null($teamId)) {
|
||||||
|
|
@ -674,6 +675,7 @@ public function update_server(Request $request)
|
||||||
'deployment_queue_limit' => 'integer|min:1',
|
'deployment_queue_limit' => 'integer|min:1',
|
||||||
'server_disk_usage_notification_threshold' => 'integer|min:1|max:100',
|
'server_disk_usage_notification_threshold' => 'integer|min:1|max:100',
|
||||||
'server_disk_usage_check_frequency' => 'string',
|
'server_disk_usage_check_frequency' => 'string',
|
||||||
|
'connection_timeout' => 'integer|min:1|max:300',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||||
|
|
@ -718,7 +720,7 @@ public function update_server(Request $request)
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
$advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']);
|
$advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'connection_timeout']);
|
||||||
if (! empty($advancedSettings)) {
|
if (! empty($advancedSettings)) {
|
||||||
$server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value)));
|
$server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ class Show extends Component
|
||||||
|
|
||||||
public string $port;
|
public string $port;
|
||||||
|
|
||||||
|
public int $connectionTimeout;
|
||||||
|
|
||||||
public ?string $validationLogs = null;
|
public ?string $validationLogs = null;
|
||||||
|
|
||||||
public ?string $wildcardDomain = null;
|
public ?string $wildcardDomain = null;
|
||||||
|
|
@ -110,6 +112,7 @@ protected function rules(): array
|
||||||
'ip' => ['required', new ValidServerIp],
|
'ip' => ['required', new ValidServerIp],
|
||||||
'user' => ['required', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
'user' => ['required', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
||||||
'port' => 'required|integer|between:1,65535',
|
'port' => 'required|integer|between:1,65535',
|
||||||
|
'connectionTimeout' => 'required|integer|min:1|max:300',
|
||||||
'validationLogs' => 'nullable',
|
'validationLogs' => 'nullable',
|
||||||
'wildcardDomain' => 'nullable|url',
|
'wildcardDomain' => 'nullable|url',
|
||||||
'isReachable' => 'required',
|
'isReachable' => 'required',
|
||||||
|
|
@ -138,6 +141,10 @@ protected function messages(): array
|
||||||
'ip.required' => 'The IP Address field is required.',
|
'ip.required' => 'The IP Address field is required.',
|
||||||
'user.required' => 'The User field is required.',
|
'user.required' => 'The User field is required.',
|
||||||
'port.required' => 'The Port field is required.',
|
'port.required' => 'The Port field is required.',
|
||||||
|
'connectionTimeout.required' => 'The SSH Connection Timeout field is required.',
|
||||||
|
'connectionTimeout.integer' => 'The SSH Connection Timeout must be an integer.',
|
||||||
|
'connectionTimeout.min' => 'The SSH Connection Timeout must be at least 1 second.',
|
||||||
|
'connectionTimeout.max' => 'The SSH Connection Timeout must not exceed 300 seconds.',
|
||||||
'wildcardDomain.url' => 'The Wildcard Domain must be a valid URL.',
|
'wildcardDomain.url' => 'The Wildcard Domain must be a valid URL.',
|
||||||
'sentinelToken.required' => 'The Sentinel Token field is required.',
|
'sentinelToken.required' => 'The Sentinel Token field is required.',
|
||||||
'sentinelMetricsRefreshRateSeconds.required' => 'The Metrics Refresh Rate field is required.',
|
'sentinelMetricsRefreshRateSeconds.required' => 'The Metrics Refresh Rate field is required.',
|
||||||
|
|
@ -210,6 +217,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->server->validation_logs = $this->validationLogs;
|
$this->server->validation_logs = $this->validationLogs;
|
||||||
$this->server->save();
|
$this->server->save();
|
||||||
|
|
||||||
|
$this->server->settings->connection_timeout = $this->connectionTimeout;
|
||||||
$this->server->settings->is_swarm_manager = $this->isSwarmManager;
|
$this->server->settings->is_swarm_manager = $this->isSwarmManager;
|
||||||
$this->server->settings->wildcard_domain = $this->wildcardDomain;
|
$this->server->settings->wildcard_domain = $this->wildcardDomain;
|
||||||
$this->server->settings->is_swarm_worker = $this->isSwarmWorker;
|
$this->server->settings->is_swarm_worker = $this->isSwarmWorker;
|
||||||
|
|
@ -237,6 +245,7 @@ public function syncData(bool $toModel = false)
|
||||||
$this->ip = $this->server->ip;
|
$this->ip = $this->server->ip;
|
||||||
$this->user = $this->server->user;
|
$this->user = $this->server->user;
|
||||||
$this->port = $this->server->port;
|
$this->port = $this->server->port;
|
||||||
|
$this->connectionTimeout = $this->server->settings->connection_timeout;
|
||||||
|
|
||||||
$this->wildcardDomain = $this->server->settings->wildcard_domain;
|
$this->wildcardDomain = $this->server->settings->wildcard_domain;
|
||||||
$this->isReachable = $this->server->settings->is_reachable;
|
$this->isReachable = $this->server->settings->is_reachable;
|
||||||
|
|
@ -407,7 +416,7 @@ public function checkHetznerServerStatus(bool $manual = false)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hetznerService = new \App\Services\HetznerService($this->server->cloudProviderToken->token);
|
$hetznerService = new HetznerService($this->server->cloudProviderToken->token);
|
||||||
$serverData = $hetznerService->getServer($this->server->hetzner_server_id);
|
$serverData = $hetznerService->getServer($this->server->hetzner_server_id);
|
||||||
|
|
||||||
$this->hetznerServerStatus = $serverData['status'] ?? null;
|
$this->hetznerServerStatus = $serverData['status'] ?? null;
|
||||||
|
|
@ -471,7 +480,7 @@ public function startHetznerServer()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$hetznerService = new \App\Services\HetznerService($this->server->cloudProviderToken->token);
|
$hetznerService = new HetznerService($this->server->cloudProviderToken->token);
|
||||||
$hetznerService->powerOnServer($this->server->hetzner_server_id);
|
$hetznerService->powerOnServer($this->server->hetzner_server_id);
|
||||||
|
|
||||||
$this->hetznerServerStatus = 'starting';
|
$this->hetznerServerStatus = 'starting';
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
'updated_at' => ['type' => 'string'],
|
'updated_at' => ['type' => 'string'],
|
||||||
'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'],
|
'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'],
|
||||||
'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
|
'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
|
||||||
|
'connection_timeout' => ['type' => 'integer', 'description' => 'SSH connection timeout in seconds.'],
|
||||||
]
|
]
|
||||||
)]
|
)]
|
||||||
class ServerSetting extends Model
|
class ServerSetting extends Model
|
||||||
|
|
@ -97,6 +98,7 @@ class ServerSetting extends Model
|
||||||
'is_terminal_enabled',
|
'is_terminal_enabled',
|
||||||
'deployment_queue_limit',
|
'deployment_queue_limit',
|
||||||
'disable_application_image_retention',
|
'disable_application_image_retention',
|
||||||
|
'connection_timeout',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|
@ -108,6 +110,7 @@ class ServerSetting extends Model
|
||||||
'is_usable' => 'boolean',
|
'is_usable' => 'boolean',
|
||||||
'is_terminal_enabled' => 'boolean',
|
'is_terminal_enabled' => 'boolean',
|
||||||
'disable_application_image_retention' => 'boolean',
|
'disable_application_image_retention' => 'boolean',
|
||||||
|
'connection_timeout' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function booted()
|
protected static function booted()
|
||||||
|
|
|
||||||
|
|
@ -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(): void
|
||||||
|
{
|
||||||
|
Schema::table('server_settings', function (Blueprint $table) {
|
||||||
|
$table->integer('connection_timeout')->default(10)->after('deployment_queue_limit');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('server_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('connection_timeout');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -10545,6 +10545,10 @@
|
||||||
"server_disk_usage_check_frequency": {
|
"server_disk_usage_check_frequency": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Cron expression for disk usage check frequency."
|
"description": "Cron expression for disk usage check frequency."
|
||||||
|
},
|
||||||
|
"connection_timeout": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "SSH connection timeout in seconds (1-300). Default: 10."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -13349,6 +13353,10 @@
|
||||||
"delete_unused_networks": {
|
"delete_unused_networks": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "The flag to indicate if the unused networks should be deleted."
|
"description": "The flag to indicate if the unused networks should be deleted."
|
||||||
|
},
|
||||||
|
"connection_timeout": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "SSH connection timeout in seconds."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
|
||||||
|
|
@ -6734,6 +6734,9 @@ paths:
|
||||||
server_disk_usage_check_frequency:
|
server_disk_usage_check_frequency:
|
||||||
type: string
|
type: string
|
||||||
description: 'Cron expression for disk usage check frequency.'
|
description: 'Cron expression for disk usage check frequency.'
|
||||||
|
connection_timeout:
|
||||||
|
type: integer
|
||||||
|
description: 'SSH connection timeout in seconds (1-300). Default: 10.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
|
|
@ -8538,6 +8541,9 @@ components:
|
||||||
delete_unused_networks:
|
delete_unused_networks:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'The flag to indicate if the unused networks should be deleted.'
|
description: 'The flag to indicate if the unused networks should be deleted.'
|
||||||
|
connection_timeout:
|
||||||
|
type: integer
|
||||||
|
description: 'SSH connection timeout in seconds.'
|
||||||
type: object
|
type: object
|
||||||
Service:
|
Service:
|
||||||
description: 'Service model'
|
description: 'Service model'
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,12 @@ class="mt-8 mb-4 w-full font-bold box-without-bg bg-coollabs hover:bg-coollabs-1
|
||||||
label="Port" required :disabled="$isValidating" />
|
label="Port" required :disabled="$isValidating" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-full lg:w-64">
|
||||||
|
<x-forms.input canGate="update" :canResource="$server" type="number"
|
||||||
|
id="connectionTimeout" label="SSH Connection Timeout (s)"
|
||||||
|
helper="Seconds to wait for SSH connection before failing. Default: 10."
|
||||||
|
min="1" max="300" required :disabled="$isValidating" />
|
||||||
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<label for="serverTimezone">Server Timezone</label>
|
<label for="serverTimezone">Server Timezone</label>
|
||||||
|
|
|
||||||
74
tests/Feature/ServerConnectionTimeoutApiTest.php
Normal file
74
tests/Feature/ServerConnectionTimeoutApiTest.php
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\InstanceSettings;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
|
||||||
|
|
||||||
|
$this->team = Team::factory()->create();
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
$this->server = Server::factory()->create([
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newToken = $this->user->createToken('write-token', ['write']);
|
||||||
|
$newToken->accessToken->forceFill(['team_id' => $this->team->id])->save();
|
||||||
|
$this->token = $newToken->plainTextToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH updates connection_timeout via API', function () {
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])->patchJson('/api/v1/servers/'.$this->server->uuid, [
|
||||||
|
'connection_timeout' => 45,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(201);
|
||||||
|
expect($this->server->settings->fresh()->connection_timeout)->toBe(45);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH rejects connection_timeout out of range', function () {
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])->patchJson('/api/v1/servers/'.$this->server->uuid, [
|
||||||
|
'connection_timeout' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$response->assertJsonStructure(['errors' => ['connection_timeout']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH rejects connection_timeout above max', function () {
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])->patchJson('/api/v1/servers/'.$this->server->uuid, [
|
||||||
|
'connection_timeout' => 999,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$response->assertJsonStructure(['errors' => ['connection_timeout']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH rejects non-integer connection_timeout', function () {
|
||||||
|
$response = $this->withHeaders([
|
||||||
|
'Authorization' => 'Bearer '.$this->token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])->patchJson('/api/v1/servers/'.$this->server->uuid, [
|
||||||
|
'connection_timeout' => 'fast',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(422);
|
||||||
|
$response->assertJsonStructure(['errors' => ['connection_timeout']]);
|
||||||
|
});
|
||||||
43
tests/Feature/ServerConnectionTimeoutTest.php
Normal file
43
tests/Feature/ServerConnectionTimeoutTest.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Helpers\SshMultiplexingHelper;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->team = $user->teams()->first();
|
||||||
|
$this->server = Server::factory()->create([
|
||||||
|
'team_id' => $this->team->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults connection_timeout to 10 seconds for new servers', function () {
|
||||||
|
expect($this->server->settings->connection_timeout)->toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists a custom connection_timeout value', function () {
|
||||||
|
$this->server->settings->connection_timeout = 30;
|
||||||
|
$this->server->settings->save();
|
||||||
|
|
||||||
|
expect($this->server->settings->fresh()->connection_timeout)->toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the per-server connection_timeout from getConnectionTimeout', function () {
|
||||||
|
$this->server->settings->connection_timeout = 45;
|
||||||
|
$this->server->settings->save();
|
||||||
|
|
||||||
|
expect(SshMultiplexingHelper::getConnectionTimeout($this->server->fresh()))->toBe(45);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to config default when connection_timeout is invalid', function () {
|
||||||
|
$this->server->settings->connection_timeout = 0;
|
||||||
|
$this->server->settings->saveQuietly();
|
||||||
|
|
||||||
|
$expected = (int) config('constants.ssh.connection_timeout');
|
||||||
|
|
||||||
|
expect(SshMultiplexingHelper::getConnectionTimeout($this->server->fresh()))->toBe($expected);
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,8 @@ import vue from "@vitejs/plugin-vue";
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), '')
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
|
const viteHost = env.VITE_HOST || null;
|
||||||
|
const vitePort = Number(env.VITE_PORT || 5173);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
server: {
|
server: {
|
||||||
|
|
@ -14,9 +16,11 @@ export default defineConfig(({ mode }) => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
hmr: {
|
allowedHosts: true,
|
||||||
host: env.VITE_HOST || '0.0.0.0'
|
origin: viteHost ? `http://${viteHost}:${vitePort}` : undefined,
|
||||||
},
|
hmr: viteHost
|
||||||
|
? { host: viteHost, clientPort: vitePort }
|
||||||
|
: true,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
laravel({
|
laravel({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue