diff --git a/ZB.MOM.WW.Secrets/CHANGELOG.md b/ZB.MOM.WW.Secrets/CHANGELOG.md index 3522f3a..00cbe84 100644 --- a/ZB.MOM.WW.Secrets/CHANGELOG.md +++ b/ZB.MOM.WW.Secrets/CHANGELOG.md @@ -5,6 +5,32 @@ follows [Keep a Changelog](https://keepachangelog.com/); versions are shared acr package in the family (see `Directory.Build.props`) even when a given release only touches one of them. +## 0.6.2 — 2026-08-11 + +### Changed + +- **The under-content-root rejection message now teaches the container case.** Suggested by the + OtOpcUa session, which is the only real containerized deployment in this family and the one that + hit the false positive. A store on a volume mounted inside the content root is durable across + container replacement but is rejected anyway, because the rule cannot tell a mount from an + ordinary directory. The message now says so, gives the fix (mount the same volume at a second + path outside the content root; the file sits at the volume root either way, so an existing store + carries over), and forecloses the wrong fix by stating that no override exists. + + A false positive's entire cost is the confusion in the moment, so a boot error that names its own + fix costs nothing on the true-positive path and converts the false positive into a two-minute + edit. **Message only — no behaviour change.** Existing 0.6.1 consumers can take this whenever they + next touch the pin; it does not warrant a re-pin round on its own. + + Their argument for keeping the rule strict is recorded because it is better than the trade-off I + had written down: the workaround is an improvement rather than a concession (it separates the + application from its durable state at the filesystem level — the exact distinction the incident + blurred); an `AllowStoreUnderContentRoot` flag has the lifecycle of a knob set once to clear a + symptom and never revisited, surviving the redeploy that changes the layout it was reasoned + against; and mount detection would make the verdict depend on runtime platform state, so the same + path would pass on one host and fail on another — strictly worse than a stable false positive + with a cheap documented fix. + ## 0.6.1 — 2026-08-11 ### Fixed diff --git a/ZB.MOM.WW.Secrets/Directory.Build.props b/ZB.MOM.WW.Secrets/Directory.Build.props index 67d33d2..de1e817 100644 --- a/ZB.MOM.WW.Secrets/Directory.Build.props +++ b/ZB.MOM.WW.Secrets/Directory.Build.props @@ -5,7 +5,7 @@ enable enable latest - 0.6.1 + 0.6.2 true README.md diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretsOptionsValidator.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretsOptionsValidator.cs index 2027495..2b37da4 100644 --- a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretsOptionsValidator.cs +++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretsOptionsValidator.cs @@ -80,7 +80,12 @@ public sealed class SecretsOptionsValidator : OptionsValidatorBase f.Contains("content root", StringComparison.Ordinal)); } + [Fact] + public void The_rejection_message_teaches_the_container_case() + { + // A false positive's entire cost is the confusion in the moment, and the known one is a + // store on a volume mounted inside the content root — durable in a container, rejected + // anyway, because the rule cannot tell a mount from an ordinary directory. A boot error + // that names its own fix costs nothing on the true-positive path and turns that false + // positive into a two-minute edit. Asserted rather than left to the doc comment, because + // the message is the only part of this a stuck operator will actually read. + var env = new StubHostEnvironment { ContentRootPath = Root("app") }; + var validator = new SecretsOptionsValidator(env); + + ValidateOptionsResult result = + validator.Validate(name: null, OptionsWith(Root("app", "data", "secrets.db"))); + + Assert.True(result.Failed); + string message = Assert.Single(result.Failures!); + Assert.Contains("mounted volume", message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("OUTSIDE the content root", message, StringComparison.Ordinal); + // And that it forecloses the wrong fix, rather than leaving the reader to hunt for a flag. + Assert.Contains("not look for an override", message, StringComparison.OrdinalIgnoreCase); + } + [Fact] public void Validator_accepts_an_absolute_path_outside_the_content_root() {