diff --git a/backend/fleet/src/Fleet.Hosting/Hosting.cs b/backend/fleet/src/Fleet.Hosting/Hosting.cs index dc4b33a..66faf23 100644 --- a/backend/fleet/src/Fleet.Hosting/Hosting.cs +++ b/backend/fleet/src/Fleet.Hosting/Hosting.cs @@ -243,7 +243,8 @@ public sealed class ConfigStore throw new InvalidOperationException("config files must stay inside the config directory"); } - Directory.CreateDirectory(_directory); + // 相对路径里的子目录(例如 update/)也要建:新建的命名卷里只有镜像自带的目录 + Directory.CreateDirectory(Path.GetDirectoryName(path)!); string temp = path + ".tmp"; #pragma warning disable RS0030 // 纯采集边界的唯一豁免点:ConfigStore 只写配置目录,先写临时文件再原子替换 File.WriteAllBytes(temp, content); diff --git a/backend/probe/tests/MoShiro.Probe.Tests/ProbeTests.cs b/backend/probe/tests/MoShiro.Probe.Tests/ProbeTests.cs index d8bfa02..5ff35e8 100644 --- a/backend/probe/tests/MoShiro.Probe.Tests/ProbeTests.cs +++ b/backend/probe/tests/MoShiro.Probe.Tests/ProbeTests.cs @@ -82,4 +82,20 @@ public sealed class ProbeTests Directory.Delete(directory, recursive: true); } } + + [Fact] + public void ConfigStore_CreatesSubdirectories_ForFreshVolumes() + { + // 新建的命名卷里没有 update/:面板转来的更新清单要能写进去 + string directory = NewDirectory(); + try + { + new ConfigStore(directory).Write("update/offer.manifest", "{}"u8.ToArray()); + Assert.True(File.Exists(Path.Combine(directory, "update", "offer.manifest"))); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } } diff --git a/ci/stage-node-image.py b/ci/stage-node-image.py index e51f744..ab19e03 100644 --- a/ci/stage-node-image.py +++ b/ci/stage-node-image.py @@ -59,6 +59,9 @@ def stage(component: str, destination: Path) -> None: config = destination / "config" config.mkdir() (config / ".keep").write_text("", encoding="utf-8") + # 更新清单和请求放在这里;空的命名卷第一次挂载时会带上这个目录 + (config / "update").mkdir() + (config / "update" / ".keep").write_text("", encoding="utf-8") def main() -> None: