feat: Implement required port validation for service applications

- Added `requiredPort` property to `ServiceApplicationView` to track the required port for services.
- Introduced modal confirmation for removing required ports, including methods to confirm or cancel the action.
- Enhanced `Service` model with `getRequiredPort` and `requiresPort` methods to retrieve port information from service templates.
- Implemented `extractPortFromUrl` method in `ServiceApplication` to extract port from FQDN URLs.
- Updated frontend views to display warnings when required ports are missing from domains.
- Created unit tests for service port validation and extraction logic, ensuring correct behavior for various scenarios.
- Added feature tests for Livewire component handling of domain submissions with required ports.
This commit is contained in:
Andras Bacsai
2025-11-06 14:32:36 +01:00
parent e21b1e40bc
commit bcd225bd22
13 changed files with 938 additions and 33 deletions
@@ -22,6 +22,12 @@ class EditDomain extends Component
public $forceSaveDomains = false;
public $showPortWarningModal = false;
public $forceRemovePort = false;
public $requiredPort = null;
#[Validate(['nullable'])]
public ?string $fqdn = null;
@@ -33,6 +39,7 @@ class EditDomain extends Component
{
$this->application = ServiceApplication::ownedByCurrentTeam()->findOrFail($this->applicationId);
$this->authorize('view', $this->application);
$this->requiredPort = $this->application->service->getRequiredPort();
$this->syncData();
}
@@ -58,6 +65,19 @@ class EditDomain extends Component
$this->submit();
}
public function confirmRemovePort()
{
$this->forceRemovePort = true;
$this->showPortWarningModal = false;
$this->submit();
}
public function cancelRemovePort()
{
$this->showPortWarningModal = false;
$this->syncData(); // Reset to original FQDN
}
public function submit()
{
try {
@@ -91,6 +111,41 @@ class EditDomain extends Component
$this->forceSaveDomains = false;
}
// Check for required port
if (! $this->forceRemovePort) {
$service = $this->application->service;
$requiredPort = $service->getRequiredPort();
if ($requiredPort !== null) {
// Check if all FQDNs have a port
$fqdns = str($this->fqdn)->trim()->explode(',');
$missingPort = false;
foreach ($fqdns as $fqdn) {
$fqdn = trim($fqdn);
if (empty($fqdn)) {
continue;
}
$port = ServiceApplication::extractPortFromUrl($fqdn);
if ($port === null) {
$missingPort = true;
break;
}
}
if ($missingPort) {
$this->requiredPort = $requiredPort;
$this->showPortWarningModal = true;
return;
}
}
} else {
// Reset the force flag after using it
$this->forceRemovePort = false;
}
$this->validate();
$this->application->save();
$this->application->refresh();
@@ -30,6 +30,12 @@ class ServiceApplicationView extends Component
public $forceSaveDomains = false;
public $showPortWarningModal = false;
public $forceRemovePort = false;
public $requiredPort = null;
#[Validate(['nullable'])]
public ?string $humanName = null;
@@ -129,12 +135,26 @@ class ServiceApplicationView extends Component
try {
$this->parameters = get_route_parameters();
$this->authorize('view', $this->application);
$this->requiredPort = $this->application->service->getRequiredPort();
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function confirmRemovePort()
{
$this->forceRemovePort = true;
$this->showPortWarningModal = false;
$this->submit();
}
public function cancelRemovePort()
{
$this->showPortWarningModal = false;
$this->syncData(); // Reset to original FQDN
}
public function syncData(bool $toModel = false): void
{
if ($toModel) {
@@ -246,6 +266,41 @@ class ServiceApplicationView extends Component
$this->forceSaveDomains = false;
}
// Check for required port
if (! $this->forceRemovePort) {
$service = $this->application->service;
$requiredPort = $service->getRequiredPort();
if ($requiredPort !== null) {
// Check if all FQDNs have a port
$fqdns = str($this->fqdn)->trim()->explode(',');
$missingPort = false;
foreach ($fqdns as $fqdn) {
$fqdn = trim($fqdn);
if (empty($fqdn)) {
continue;
}
$port = ServiceApplication::extractPortFromUrl($fqdn);
if ($port === null) {
$missingPort = true;
break;
}
}
if ($missingPort) {
$this->requiredPort = $requiredPort;
$this->showPortWarningModal = true;
return;
}
}
} else {
// Reset the force flag after using it
$this->forceRemovePort = false;
}
$this->validate();
$this->application->save();
$this->application->refresh();
+25
View File
@@ -1184,6 +1184,31 @@ class Service extends BaseModel
return data_get($service, 'documentation', config('constants.urls.docs'));
}
/**
* Get the required port for this service from the template definition.
*/
public function getRequiredPort(): ?int
{
try {
$services = get_service_templates();
$serviceName = str($this->name)->beforeLast('-')->value();
$service = data_get($services, $serviceName, []);
$port = data_get($service, 'port');
return $port ? (int) $port : null;
} catch (\Throwable) {
return null;
}
}
/**
* Check if this service requires a port to function correctly.
*/
public function requiresPort(): bool
{
return $this->getRequiredPort() !== null;
}
public function applications()
{
return $this->hasMany(ServiceApplication::class);
+47
View File
@@ -118,6 +118,53 @@ class ServiceApplication extends BaseModel
);
}
/**
* Extract port number from a given FQDN URL.
* Returns null if no port is specified.
*/
public static function extractPortFromUrl(string $url): ?int
{
try {
// Ensure URL has a scheme for proper parsing
if (! str_starts_with($url, 'http://') && ! str_starts_with($url, 'https://')) {
$url = 'http://'.$url;
}
$parsed = parse_url($url);
$port = $parsed['port'] ?? null;
return $port ? (int) $port : null;
} catch (\Throwable) {
return null;
}
}
/**
* Check if all FQDNs have a port specified.
*/
public function allFqdnsHavePort(): bool
{
if (is_null($this->fqdn) || $this->fqdn === '') {
return false;
}
$fqdns = explode(',', $this->fqdn);
foreach ($fqdns as $fqdn) {
$fqdn = trim($fqdn);
if (empty($fqdn)) {
continue;
}
$port = self::extractPortFromUrl($fqdn);
if ($port === null) {
return false;
}
}
return true;
}
public function getFilesFromServer(bool $isInit = false)
{
getFilesystemVolumesFromServer($this, $isInit);