-
-
Notifications
You must be signed in to change notification settings - Fork 5k
Expand file tree
/
Copy pathLocalPersistentVolume.php
More file actions
194 lines (161 loc) · 5.95 KB
/
Copy pathLocalPersistentVolume.php
File metadata and controls
194 lines (161 loc) · 5.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Symfony\Component\Yaml\Yaml;
class LocalPersistentVolume extends BaseModel
{
protected $fillable = [
'name',
'mount_path',
'host_path',
'container_id',
'resource_type',
'resource_id',
'is_preview_suffix_enabled',
];
protected $casts = [
'is_preview_suffix_enabled' => 'boolean',
];
public function resource()
{
return $this->morphTo('resource');
}
public function application()
{
return $this->morphTo('resource');
}
public function service()
{
return $this->morphTo('resource');
}
public function database()
{
return $this->morphTo('resource');
}
protected function customizeName($value)
{
return str($value)->trim()->value;
}
protected function mountPath(): Attribute
{
return Attribute::make(
set: fn (string $value) => str($value)->trim()->start('/')->value
);
}
protected function hostPath(): Attribute
{
return Attribute::make(
set: function (?string $value) {
if ($value) {
return str($value)->trim()->start('/')->value;
} else {
return $value;
}
}
);
}
// 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();
}
// 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
// Note: We match on mount_path (container path) only, since host paths get transformed
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';
}
}
} 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;
}
}
}
return false;
} catch (\Throwable $e) {
return false;
}
}
}