Skip to content

Commit 52a7666

Browse files
committed
feat: implement centralized override management for compose files
1 parent 6de03f0 commit 52a7666

5 files changed

Lines changed: 162 additions & 110 deletions

File tree

source/compose.manager/php/compose_list.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
$stackCount = 0;
4545

4646
foreach ($composeProjects as $project) {
47+
// Skip if not a directory or if it doesn't contain a compose file (either directly or via indirect)
4748
if (!hasComposeFile("$compose_root/$project") &&
4849
(! is_file("$compose_root/$project/indirect"))
4950
) {
@@ -64,15 +65,17 @@
6465
? trim(file_get_contents("$compose_root/$project/indirect"))
6566
: "$compose_root/$project";
6667
$composeFile = findComposeFile($basePath) ?: "$basePath/docker-compose.yml";
67-
$overrideFile = "$compose_root/$project/docker-compose.override.yml";
68+
// Resolve override via centralized helper (prefer correctly-named indirect override)
69+
require_once("/usr/local/emhttp/plugins/compose.manager/php/util.php");
70+
$overridePath = OverrideInfo::fromStack($compose_root, $project)->getOverridePath();
6871

6972
// Use docker compose config --services to get accurate service count
7073
// This properly parses YAML, handles overrides, extends, etc.
7174
$definedServices = 0;
7275
if (is_file($composeFile)) {
7376
$files = "-f " . escapeshellarg($composeFile);
74-
if (is_file($overrideFile)) {
75-
$files .= " -f " . escapeshellarg($overrideFile);
77+
if (is_file($overridePath)) {
78+
$files .= " -f " . escapeshellarg($overridePath);
7679
}
7780

7881
// Get env file if specified

source/compose.manager/php/compose_util_functions.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,11 @@ function echoComposeCommand($action, $recreate = false)
9191
$composeCommand[] = "-f$composeFile";
9292
}
9393

94-
if (is_file("$path/docker-compose.override.yml")) {
95-
$composeOverride = "-f$path/docker-compose.override.yml";
96-
$composeCommand[] = $composeOverride;
97-
}
94+
// Resolve override using centralized helper
95+
$stackName = basename($path);
96+
require_once("/usr/local/emhttp/plugins/compose.manager/php/util.php");
97+
$overridePath = OverrideInfo::fromStack($compose_root, $stackName)->getOverridePath();
98+
$composeCommand[] = "-f" . escapeshellarg($overridePath);
9899

99100
if (is_file("$path/envpath")) {
100101
$envPath = "-e" . trim(file_get_contents("$path/envpath"));
@@ -205,11 +206,10 @@ function echoComposeCommandMultiple($action, $paths)
205206
$composeCommand[] = "-f$composeFile";
206207
}
207208

208-
if (is_file("$path/docker-compose.override.yml")) {
209-
$composeOverride = "-f$path/docker-compose.override.yml";
210-
$composeCommand[] = $composeOverride;
211-
}
212-
209+
// Resolve override using centralized helper
210+
require_once("/usr/local/emhttp/plugins/compose.manager/php/util.php");
211+
$overridePath = OverrideInfo::fromStack($compose_root, $stackName)->getOverridePath();
212+
$composeCommand[] = "-f" . escapeshellarg($overridePath);
213213
if (is_file("$path/envpath")) {
214214
$envPath = "-e" . trim(file_get_contents("$path/envpath"));
215215
$composeCommand[] = $envPath;

source/compose.manager/php/exec.php

Lines changed: 10 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -72,43 +72,18 @@ function getPostScript(): string
7272
if ($indirect != "") {
7373
file_put_contents("$folder/indirect", $indirect);
7474
if (!findComposeFile($indirect)) {
75-
file_put_contents("$indirect/compose.yaml", "services:\n");
75+
file_put_contents("$indirect/" . COMPOSE_FILE_NAMES[0], "services:\n");
7676
clientDebug("[stack] Indirect compose file not found at path: $indirect. Created stack with empty compose file.", null, 'daemon', 'warning');
7777
}
7878
} else {
79-
file_put_contents("$folder/compose.yaml", "services:\n");
79+
file_put_contents("$folder/" . COMPOSE_FILE_NAMES[0], "services:\n");
8080
clientDebug("[$stackName] Compose file not found at path: $folder. Created stack with empty compose file.", null, 'daemon', 'warning');
8181
}
8282

83-
// Create initial override file if it doesn't exist (for UI labels)
84-
// Override filename must match the compose filename (e.g. compose.yaml -> compose.override.yaml)
85-
// Overrides are stored in the project folder ($folder) even for indirect stacks.
86-
$composeSource = $indirect != "" ? $indirect : $folder;
87-
$foundCompose = findComposeFile($composeSource);
88-
$composeBaseName = $foundCompose !== false ? basename($foundCompose) : 'docker-compose.yml';
89-
$overrideName = preg_replace('/(\.[^.]+)$/', '.override$1', $composeBaseName);
90-
$overrideFile = "$folder/$overrideName";
91-
92-
// Migrate legacy override filename if present in either project folder or indirect path
93-
$legacyInProject = "$folder/docker-compose.override.yml";
94-
$legacyInIndirect = ($indirect != "") ? "$indirect/docker-compose.override.yml" : null;
95-
if (!is_file($overrideFile)) {
96-
if (is_file($legacyInProject) && realpath($legacyInProject) !== realpath($overrideFile)) {
97-
@rename($legacyInProject, $overrideFile);
98-
clientDebug("[override] Migrated legacy override $legacyInProject -> $overrideFile", null, 'daemon', 'info');
99-
} elseif ($legacyInIndirect && is_file($legacyInIndirect) && realpath($legacyInIndirect) !== realpath($overrideFile)) {
100-
@rename($legacyInIndirect, $overrideFile);
101-
clientDebug("[override] Migrated legacy override $legacyInIndirect -> $overrideFile", null, 'daemon', 'info');
102-
}
103-
}
104-
105-
if (!is_file($overrideFile)) {
106-
$overrideContent = "# Override file for UI labels (icon, webui, shell)\n";
107-
$overrideContent .= "# This file is managed by Compose Manager\n";
108-
$overrideContent .= "services: {}\n";
109-
file_put_contents($overrideFile, $overrideContent);
110-
}
83+
// Init override info to ensure override file is created for new stack (if not indirect) and to avoid errors when accessing settings before the override file is created
84+
OverrideInfo::fromStack($compose_root, $stackName);
11185

86+
// Save stack name (which may differ from folder name) for display purposes
11287
file_put_contents("$folder/name", $stackName);
11388

11489
// Save description if provided
@@ -172,7 +147,7 @@ function getPostScript(): string
172147
$script = getPostScript();
173148
$basePath = getPath("$compose_root/$script");
174149
$foundComposeFile = findComposeFile($basePath);
175-
$composeFilePath = $foundComposeFile !== false ? $foundComposeFile : "$basePath/docker-compose.yml";
150+
$composeFilePath = $foundComposeFile !== false ? $foundComposeFile : "$basePath/compose.yaml";
176151
$fileName = basename($composeFilePath);
177152

178153
if ($foundComposeFile !== false) {
@@ -221,36 +196,8 @@ function getPostScript(): string
221196
$script = getPostScript();
222197
$projectPath = "$compose_root/$script";
223198

224-
// If indirect exists, use it to discover the compose filename, but
225-
// overrides are stored in the project folder ($projectPath).
226-
$indirectPath = is_file("$projectPath/indirect") ? file_get_contents("$projectPath/indirect") : "";
227-
$indirectPath = str_replace("\r", "", $indirectPath);
228-
$composeSource = $indirectPath !== "" ? $indirectPath : $projectPath;
229-
230-
$foundCompose = findComposeFile($composeSource);
231-
$composeBaseName = $foundCompose !== false ? basename($foundCompose) : 'docker-compose.yml';
232-
$fileName = preg_replace('/(\.[^.]+)$/', '.override$1', $composeBaseName);
233-
234-
$overridePath = "$projectPath/$fileName";
235-
$legacyProject = "$projectPath/docker-compose.override.yml";
236-
$legacyIndirect = ($indirectPath !== "") ? "$indirectPath/docker-compose.override.yml" : null;
237-
238-
// Prefer correctly-named indirect override if present (do NOT move/rename indirect files)
239-
if ($indirectPath !== "" && is_file("$indirectPath/$fileName")) {
240-
$overridePath = "$indirectPath/$fileName";
241-
} else {
242-
// If indirect has a legacy-named override, warn the user and fall back to project override
243-
if ($indirectPath !== "" && $legacyIndirect && is_file($legacyIndirect)) {
244-
clientDebug("[override] Indirect override exists with non-matching name ($legacyIndirect). Using project fallback.", null, 'daemon', 'warning');
245-
}
246-
247-
// Migrate legacy project override to computed name if present (project-only migration)
248-
if (!is_file($projectPath.'/'.$fileName) && is_file($legacyProject) && realpath($legacyProject) !== realpath($projectPath.'/'.$fileName)) {
249-
@rename($legacyProject, $projectPath.'/'.$fileName);
250-
clientDebug("[override] Migrated legacy project override $legacyProject -> $projectPath/$fileName", null, 'daemon', 'info');
251-
}
252-
$overridePath = "$projectPath/$fileName";
253-
}
199+
// Get Override file path and ensure project override exists (create blank if not)
200+
$overridePath = OverrideInfo::fromStack($compose_root, $script)->getOverridePath();
254201

255202
$scriptContents = is_file($overridePath) ? file_get_contents($overridePath) : "";
256203
$scriptContents = str_replace("\r", "", $scriptContents);
@@ -287,38 +234,8 @@ function getPostScript(): string
287234
$scriptContents = isset($_POST['scriptContents']) ? $_POST['scriptContents'] : "";
288235
$projectPath = "$compose_root/$script";
289236

290-
// Determine compose source (indirect or project) for filename, but
291-
// always store override in project folder.
292-
$indirectPath = is_file("$projectPath/indirect") ? file_get_contents("$projectPath/indirect") : "";
293-
$indirectPath = str_replace("\r", "", $indirectPath);
294-
$composeSource = $indirectPath !== "" ? $indirectPath : $projectPath;
295-
296-
$foundCompose = findComposeFile($composeSource);
297-
$composeBaseName = $foundCompose !== false ? basename($foundCompose) : 'docker-compose.yml';
298-
$fileName = preg_replace('/(\.[^.]+)$/', '.override$1', $composeBaseName);
299-
300-
// Determine where to save: if indirect has a correctly-named override, save there (do NOT rename/move indirect files).
301-
$indirectOverridePath = ($indirectPath !== "") ? "$indirectPath/$fileName" : null;
302-
$legacyIndirect = ($indirectPath !== "") ? "$indirectPath/docker-compose.override.yml" : null;
303-
$legacyProject = "$projectPath/docker-compose.override.yml";
304-
305-
if ($indirectOverridePath && is_file($indirectOverridePath)) {
306-
// Save back to the indirect override file the user provided (preserve external file)
307-
$overridePath = $indirectOverridePath;
308-
} else {
309-
// If an indirect legacy-named override exists, warn and save to project fallback (do not move it)
310-
if ($legacyIndirect && is_file($legacyIndirect)) {
311-
clientDebug("[override] Indirect override exists with non-matching name ($legacyIndirect). Saving to project override instead.", null, 'daemon', 'warning');
312-
}
313-
314-
// Migrate legacy project override to computed name if present (project-only migration)
315-
if (!is_file("$projectPath/$fileName") && is_file($legacyProject) && realpath($legacyProject) !== realpath("$projectPath/$fileName")) {
316-
@rename($legacyProject, "$projectPath/$fileName");
317-
clientDebug("[override] Migrated legacy project override $legacyProject -> $projectPath/$fileName", null, 'daemon', 'info');
318-
}
319-
320-
$overridePath = "$projectPath/$fileName";
321-
}
237+
// Get Override file path and ensure project override exists (create blank if not)
238+
$overridePath = OverrideInfo::fromStack($compose_root, $script)->getOverridePath();
322239

323240
file_put_contents($overridePath, $scriptContents);
324241
echo "$overridePath saved";

source/compose.manager/php/exec_functions.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ function buildComposeArgs(string $stack): array {
7979
$projectName = sanitizeStr($projectName);
8080

8181
$basePath = getPath("$compose_root/$stack");
82-
$composeFile = findComposeFile($basePath) ?: "$basePath/docker-compose.yml";
83-
$overrideFile = "$compose_root/$stack/docker-compose.override.yml";
82+
$composeFile = findComposeFile($basePath) ?: "$basePath/compose.yaml";
8483

8584
$files = "-f " . escapeshellarg($composeFile);
86-
if (is_file($overrideFile)) {
87-
$files .= " -f " . escapeshellarg($overrideFile);
88-
}
85+
86+
// Resolve override selection: prefer correctly-named indirect override if present,
87+
// otherwise use project override (migrating legacy project override when applicable).
88+
require_once("/usr/local/emhttp/plugins/compose.manager/php/util.php");
89+
$overridePath = OverrideInfo::fromStack($compose_root, $stack)->getOverridePath();
90+
$files .= " -f " . escapeshellarg($overridePath);
8991

9092
$envFile = "";
9193
if (is_file("$compose_root/$stack/envpath")) {

source/compose.manager/php/util.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,136 @@ function hasComposeFile($dir)
110110
return findComposeFile($dir) !== false;
111111
}
112112

113+
class OverrideInfo
114+
{
115+
/**
116+
* @var string Computed override filename (e.g. compose.override.yaml)
117+
*/
118+
public string $computedName = '';
119+
/**
120+
* @var string|null Path to project override file
121+
*/
122+
public ?string $projectOverride = null;
123+
/**
124+
* @var string|null Path to indirect override file
125+
*/
126+
public ?string $indirectOverride = null;
127+
/**
128+
* @var bool True if indirect override should be used
129+
*/
130+
public bool $useIndirect = false;
131+
/**
132+
* @var bool True if indirect contains legacy-named override but not correctly-named one
133+
*/
134+
public bool $mismatchIndirectLegacy = false;
135+
136+
/**
137+
* @var string Compose root directory
138+
*/
139+
private string $composeRoot;
140+
141+
/**
142+
* @var string Stack name (folder name under compose root) - used for generating override template path
143+
*/
144+
private string $stack;
145+
146+
/**
147+
* Constructor
148+
* @param string $composeRoot Compose root directory
149+
*/
150+
private function __construct(string $composeRoot)
151+
{
152+
$this->composeRoot = rtrim($composeRoot, "/");
153+
}
154+
155+
/**
156+
* Static factory to create and resolve an OverrideInfo for a stack.
157+
* @param string $composeRoot
158+
* @param string $stack
159+
* @return OverrideInfo
160+
*/
161+
public static function fromStack(string $composeRoot, string $stack): OverrideInfo
162+
{
163+
$info = new self($composeRoot);
164+
$info->resolve($stack);
165+
return $info;
166+
}
167+
168+
/**
169+
* Resolve override information for a given stack and populate this instance.
170+
* @param string $stack
171+
* @return void
172+
*/
173+
private function resolve(string $stack): void
174+
{
175+
$this->stack = $stack;
176+
$projectPath = $this->getProjectPath($stack);
177+
$indirectPath = is_file("$projectPath/indirect") ? trim(file_get_contents("$projectPath/indirect")) : null;
178+
$composeSource = $indirectPath !== "" ? $indirectPath : $projectPath;
179+
180+
$foundCompose = findComposeFile($composeSource);
181+
$composeBaseName = $foundCompose !== false ? basename($foundCompose) : COMPOSE_FILE_NAMES[0];
182+
$this->computedName = preg_replace('/(\.[^.]+)$/', '.override$1', $composeBaseName);
183+
184+
$this->projectOverride = $projectPath . '/' . $this->computedName;
185+
$this->indirectOverride = $indirectPath !== "" ? ($indirectPath . '/' . $this->computedName) : null;
186+
187+
$legacyProject = $projectPath . '/docker-compose.override.yml';
188+
$legacyIndirect = $indirectPath !== "" ? ($indirectPath . '/docker-compose.override.yml') : null;
189+
190+
$this->useIndirect = ($this->indirectOverride && is_file($this->indirectOverride));
191+
$this->mismatchIndirectLegacy = ($indirectPath !== "" && $legacyIndirect && is_file($legacyIndirect) && !($this->indirectOverride && is_file($this->indirectOverride)));
192+
193+
// Migrate legacy project override to computed project override (project-only migration)
194+
if (!is_file($this->projectOverride) && is_file($legacyProject) && realpath($legacyProject) !== @realpath($this->projectOverride)) {
195+
@rename($legacyProject, $this->projectOverride);
196+
clientDebug("[override] Migrated legacy project override $legacyProject -> $this->projectOverride", null, 'daemon', 'info');
197+
}
198+
199+
if (is_file($this->projectOverride) && is_file($legacyProject) && realpath($legacyProject) !== @realpath($this->projectOverride)) {
200+
@rename($legacyProject, $legacyProject . ".bak");
201+
clientDebug("[override] Removed stale legacy project override $legacyProject (mismatch with computed override)", null, 'daemon', 'info');
202+
}
203+
204+
if ($this->mismatchIndirectLegacy) {
205+
clientDebug("[override] Indirect override exists with non-matching name; using project fallback.", null, 'daemon', 'warning');
206+
}
207+
208+
if (!is_file($this->projectOverride) && !$this->useIndirect) {
209+
$overrideContent = "# Override file for UI labels (icon, webui, shell)\n";
210+
$overrideContent .= "# This file is managed by Compose Manager\n";
211+
$overrideContent .= "services: {}\n";
212+
file_put_contents($this->projectOverride, $overrideContent);
213+
clientDebug("[override] Created missing project override template at $this->projectOverride", null, 'daemon', 'info');
214+
}
215+
}
216+
217+
/**
218+
* Get the override file to use (indirect if present, else project override)
219+
* @return string|null
220+
*/
221+
public function getOverridePath(): ?string
222+
{
223+
return $this->useIndirect ? $this->indirectOverride : $this->projectOverride;
224+
}
225+
226+
/**
227+
* Get the project path for a stack
228+
* @param string $stack
229+
* @return string
230+
*/
231+
private function getProjectPath(string $stack): string
232+
{
233+
return $this->composeRoot . '/' . $stack;
234+
}
235+
}
236+
237+
238+
239+
240+
241+
242+
113243
/**
114244
* Stack operation locking functions
115245
* Prevents concurrent operations on the same stack

0 commit comments

Comments
 (0)