2023-04-03 11:37:53 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Models;
|
|
|
|
|
|
2023-05-05 10:08:38 +00:00
|
|
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
2023-06-15 11:28:16 +00:00
|
|
|
use Illuminate\Database\Eloquent\Model;
|
2025-10-03 14:39:57 +00:00
|
|
|
use Symfony\Component\Yaml\Yaml;
|
2023-05-05 10:08:38 +00:00
|
|
|
|
2023-06-15 11:28:16 +00:00
|
|
|
class LocalPersistentVolume extends Model
|
2023-04-03 11:37:53 +00:00
|
|
|
{
|
2023-08-07 20:14:21 +00:00
|
|
|
protected $guarded = [];
|
2023-08-08 09:51:36 +00:00
|
|
|
|
2025-12-11 20:23:46 +00:00
|
|
|
public function resource()
|
|
|
|
|
{
|
|
|
|
|
return $this->morphTo('resource');
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-03 11:37:53 +00:00
|
|
|
public function application()
|
|
|
|
|
{
|
2023-09-25 15:14:19 +00:00
|
|
|
return $this->morphTo('resource');
|
2023-04-03 11:37:53 +00:00
|
|
|
}
|
2024-06-10 20:43:34 +00:00
|
|
|
|
2023-09-20 13:42:41 +00:00
|
|
|
public function service()
|
|
|
|
|
{
|
2023-09-25 15:14:19 +00:00
|
|
|
return $this->morphTo('resource');
|
|
|
|
|
}
|
2024-06-10 20:43:34 +00:00
|
|
|
|
2023-09-25 15:14:19 +00:00
|
|
|
public function database()
|
|
|
|
|
{
|
|
|
|
|
return $this->morphTo('resource');
|
2023-09-20 13:42:41 +00:00
|
|
|
}
|
2024-06-10 20:43:34 +00:00
|
|
|
|
2025-08-19 10:14:48 +00:00
|
|
|
protected function customizeName($value)
|
2023-05-05 10:08:38 +00:00
|
|
|
{
|
2025-08-19 10:14:48 +00:00
|
|
|
return str($value)->trim()->value;
|
2023-05-05 10:08:38 +00:00
|
|
|
}
|
2023-08-08 09:51:36 +00:00
|
|
|
|
2023-05-05 10:08:38 +00:00
|
|
|
protected function mountPath(): Attribute
|
|
|
|
|
{
|
|
|
|
|
return Attribute::make(
|
2024-06-25 08:37:10 +00:00
|
|
|
set: fn (string $value) => str($value)->trim()->start('/')->value
|
2023-05-05 10:08:38 +00:00
|
|
|
);
|
|
|
|
|
}
|
2023-08-08 09:51:36 +00:00
|
|
|
|
2023-05-05 10:08:38 +00:00
|
|
|
protected function hostPath(): Attribute
|
|
|
|
|
{
|
|
|
|
|
return Attribute::make(
|
2024-06-10 20:43:34 +00:00
|
|
|
set: function (?string $value) {
|
2023-05-05 10:08:38 +00:00
|
|
|
if ($value) {
|
2024-06-25 08:37:10 +00:00
|
|
|
return str($value)->trim()->start('/')->value;
|
2025-01-07 14:31:43 +00:00
|
|
|
} else {
|
|
|
|
|
return $value;
|
2023-05-05 10:08:38 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-03 14:39:57 +00:00
|
|
|
|
2025-12-11 20:23:46 +00:00
|
|
|
// Check if this volume belongs to a service resource
|
|
|
|
|
public function isServiceResource(): bool
|
|
|
|
|
{
|
|
|
|
|
return in_array($this->resource_type, [
|
|
|
|
|
'App\Models\ServiceApplication',
|
|
|
|
|
'App\Models\ServiceDatabase',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if this volume belongs to a dockercompose application
|
|
|
|
|
public function isDockerComposeResource(): bool
|
|
|
|
|
{
|
|
|
|
|
if ($this->resource_type !== 'App\Models\Application') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only access relationship if already eager loaded to avoid N+1
|
|
|
|
|
if (! $this->relationLoaded('resource')) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$application = $this->resource;
|
|
|
|
|
if (! $application) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data_get($application, 'build_pack') === 'dockercompose';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine if this volume should be read-only in the UI
|
|
|
|
|
// Service volumes and dockercompose application volumes are read-only
|
|
|
|
|
// (users should edit compose file directly)
|
|
|
|
|
public function shouldBeReadOnlyInUI(): bool
|
|
|
|
|
{
|
|
|
|
|
// All service volumes should be read-only in UI
|
|
|
|
|
if ($this->isServiceResource()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All dockercompose application volumes should be read-only in UI
|
|
|
|
|
if ($this->isDockerComposeResource()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for explicit :ro flag in compose (existing logic)
|
|
|
|
|
return $this->isReadOnlyVolume();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 14:39:57 +00:00
|
|
|
// Check if this volume is read-only by parsing the docker-compose content
|
|
|
|
|
public function isReadOnlyVolume(): bool
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
// Get the resource (can be application, service, or database)
|
|
|
|
|
$resource = $this->resource;
|
|
|
|
|
if (! $resource) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only check for services
|
|
|
|
|
if (! method_exists($resource, 'service')) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$actualService = $resource->service;
|
|
|
|
|
if (! $actualService || ! $actualService->docker_compose_raw) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the docker-compose content
|
|
|
|
|
$compose = Yaml::parse($actualService->docker_compose_raw);
|
|
|
|
|
if (! isset($compose['services'])) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the service that this volume belongs to
|
|
|
|
|
$serviceName = $resource->name;
|
|
|
|
|
if (! isset($compose['services'][$serviceName]['volumes'])) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$volumes = $compose['services'][$serviceName]['volumes'];
|
|
|
|
|
|
|
|
|
|
// Check each volume to find a match
|
2025-12-11 13:18:58 +00:00
|
|
|
// Note: We match on mount_path (container path) only, since host paths get transformed
|
2025-10-03 14:39:57 +00:00
|
|
|
foreach ($volumes as $volume) {
|
|
|
|
|
// Volume can be string like "host:container:ro" or "host:container"
|
|
|
|
|
if (is_string($volume)) {
|
|
|
|
|
$parts = explode(':', $volume);
|
|
|
|
|
|
|
|
|
|
// Check if this volume matches our mount_path
|
|
|
|
|
if (count($parts) >= 2) {
|
|
|
|
|
$containerPath = $parts[1];
|
|
|
|
|
$options = $parts[2] ?? null;
|
|
|
|
|
|
|
|
|
|
// Match based on mount_path
|
|
|
|
|
// Remove leading slash from mount_path if present for comparison
|
|
|
|
|
$mountPath = str($this->mount_path)->ltrim('/')->toString();
|
|
|
|
|
$containerPathClean = str($containerPath)->ltrim('/')->toString();
|
|
|
|
|
|
|
|
|
|
if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) {
|
|
|
|
|
return $options === 'ro';
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-11 13:18:58 +00:00
|
|
|
} elseif (is_array($volume)) {
|
|
|
|
|
// Long-form syntax: { type: bind/volume, source: ..., target: ..., read_only: true }
|
|
|
|
|
$containerPath = data_get($volume, 'target');
|
|
|
|
|
$readOnly = data_get($volume, 'read_only', false);
|
|
|
|
|
|
|
|
|
|
// Match based on mount_path
|
|
|
|
|
// Remove leading slash from mount_path if present for comparison
|
|
|
|
|
$mountPath = str($this->mount_path)->ltrim('/')->toString();
|
|
|
|
|
$containerPathClean = str($containerPath)->ltrim('/')->toString();
|
|
|
|
|
|
|
|
|
|
if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) {
|
|
|
|
|
return $readOnly === true;
|
|
|
|
|
}
|
2025-10-03 14:39:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
ray($e->getMessage(), 'Error checking read-only persistent volume');
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-03 11:37:53 +00:00
|
|
|
}
|