fix: select NPM admin port regardless of binding order
Demo images / Build & push demo images (push) Failing after 34s
Demo images / Build & push demo images (push) Failing after 34s
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
## v1.8.22-alpha (2026-09-30)
|
||||
|
||||
- Fixed Nginx Proxy Manager launch readiness choosing a proxy listener instead of its admin port after container recreation.
|
||||
|
||||
- Network diagnostic failures no longer stop all apps or rebuild shared container networking.
|
||||
- Prevented orphaned companion dashboards from repeatedly reinstalling themselves after their backend app was removed.
|
||||
- Fixed companion dashboard builds still referencing a retired image registry.
|
||||
|
||||
@@ -978,14 +978,20 @@ fn extract_lan_address(ports: &[String]) -> Option<String> {
|
||||
let mut first_candidate = None;
|
||||
for port_str in ports {
|
||||
// Parse port strings like "0.0.0.0:18443->18443/tcp" or "0.0.0.0:18443-18444->18443-18444/tcp"
|
||||
let Some(public_part) = port_str.split("->").next() else {
|
||||
let Some((public_part, _)) = port_str.split_once("->") else {
|
||||
continue;
|
||||
};
|
||||
let Some(port_part) = public_part.split(':').nth(1) else {
|
||||
let Some((_, port_part)) = public_part.rsplit_once(':') else {
|
||||
continue;
|
||||
};
|
||||
// Extract just the first port if it's a range (e.g., "18443-18444" -> "18443")
|
||||
let host_port = port_part.split('-').next().unwrap_or(port_part);
|
||||
let Ok(host_port) = host_port.parse::<u16>() else {
|
||||
continue;
|
||||
};
|
||||
if host_port == 0 {
|
||||
continue;
|
||||
}
|
||||
let candidate = format!("http://localhost:{}", host_port);
|
||||
if first_candidate.is_none() {
|
||||
first_candidate = Some(candidate.clone());
|
||||
@@ -1113,6 +1119,29 @@ fn package_launch_candidate(
|
||||
if let Some(companion) = companion_lan_address(app_id) {
|
||||
return Some(companion);
|
||||
}
|
||||
if app_id == "nginx-proxy-manager" {
|
||||
// 80/443 serve users' proxy hosts; only container port 81 serves the
|
||||
// admin UI. Podman's binding order is unstable across recreation.
|
||||
// Resolve its actual host allocation rather than guessing the first
|
||||
// HTTP port or hardcoding the default host port 8081.
|
||||
let admin_ports: Vec<String> = ports
|
||||
.iter()
|
||||
.filter(|port| {
|
||||
port.split_once("->")
|
||||
.is_some_and(|(_, target)| target == "81/tcp")
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
// With published bindings, a missing admin mapping is not evidence
|
||||
// that some unrelated service on the default host port is this UI.
|
||||
return extract_lan_address(&admin_ports).or_else(|| {
|
||||
if ports.is_empty() {
|
||||
known
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
}
|
||||
if uses_allocated_launch_port(app_id) {
|
||||
extract_lan_address(ports).or(known)
|
||||
} else {
|
||||
@@ -1241,6 +1270,98 @@ mod extract_lan_address_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_admin_launch_is_independent_of_proxy_binding_order() {
|
||||
let mappings = [
|
||||
"10.77.0.2:18081->80/tcp",
|
||||
"10.77.0.2:18443->443/tcp",
|
||||
"127.0.0.1:8081->81/tcp",
|
||||
];
|
||||
for order in [
|
||||
[0, 1, 2],
|
||||
[0, 2, 1],
|
||||
[1, 0, 2],
|
||||
[1, 2, 0],
|
||||
[2, 0, 1],
|
||||
[2, 1, 0],
|
||||
] {
|
||||
let ports: Vec<String> = order.iter().map(|&i| mappings[i].into()).collect();
|
||||
assert_eq!(
|
||||
package_launch_candidate(
|
||||
"nginx-proxy-manager",
|
||||
&ports,
|
||||
Some("http://localhost:8081/".into())
|
||||
)
|
||||
.as_deref(),
|
||||
Some("http://localhost:8081")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_admin_launch_respects_host_allocation_and_ipv6_bindings() {
|
||||
for binding in ["127.0.0.1", "0.0.0.0", "[::1]", "[::]"] {
|
||||
let ports = vec![
|
||||
"10.77.0.2:18081->80/tcp".into(),
|
||||
format!("{binding}:28081->81/tcp"),
|
||||
];
|
||||
assert_eq!(
|
||||
package_launch_candidate(
|
||||
"nginx-proxy-manager",
|
||||
&ports,
|
||||
Some("http://localhost:8081/".into())
|
||||
)
|
||||
.as_deref(),
|
||||
Some("http://localhost:28081")
|
||||
);
|
||||
}
|
||||
let proxies = vec![
|
||||
"10.77.0.2:18081->80/tcp".into(),
|
||||
"10.77.0.2:18443->443/tcp".into(),
|
||||
];
|
||||
assert_eq!(
|
||||
package_launch_candidate("nginx-proxy-manager", &proxies, None),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
package_launch_candidate(
|
||||
"nginx-proxy-manager",
|
||||
&proxies,
|
||||
Some("http://localhost:8081/".into())
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_without_port_information_uses_declared_admin_url() {
|
||||
assert_eq!(
|
||||
package_launch_candidate(
|
||||
"nginx-proxy-manager",
|
||||
&[],
|
||||
Some("http://localhost:8081/".into())
|
||||
)
|
||||
.as_deref(),
|
||||
Some("http://localhost:8081/")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_published_ports_do_not_become_launch_urls() {
|
||||
for port in [
|
||||
"81/tcp",
|
||||
"127.0.0.1:bad->81/tcp",
|
||||
"[::1]:0->81/tcp",
|
||||
"[::]:65536->81/tcp",
|
||||
"127.0.0.1:8081->81/udp",
|
||||
] {
|
||||
assert_eq!(
|
||||
package_launch_candidate("nginx-proxy-manager", &[port.into()], None),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_ssh_port_when_web_port_is_published() {
|
||||
// gitea: SSH published before the web port, in podman's list order.
|
||||
|
||||
@@ -398,3 +398,35 @@ the old recovery code. A regression executes the actual installer block against
|
||||
stale disposable files twice and confirms a missing safety payload fails closed.
|
||||
The mounted-ISO smoke test also compares all three overlay files to source.
|
||||
The final ISO build captures the exact newly deployed OTA UI/runtime payload.
|
||||
|
||||
### Final kiosk acceptance found nondeterministic NPM launch selection
|
||||
|
||||
Do not publish the staged `d1bc1273` candidate. NPM itself remains healthy and its
|
||||
API, Portainer integration, site, and native services passed stability checks.
|
||||
However, final kiosk acceptance found its card stuck at "Web UI not ready".
|
||||
The runtime reported bindings in proxy-HTTP, proxy-HTTPS, admin order. The scanner
|
||||
chose the first non-database/SSH binding, then rejected its tunnel-only host port
|
||||
as unreachable on loopback, leaving the launch address empty. Earlier tests had
|
||||
passed with admin first. This is a confirmed order-dependent scanner defect.
|
||||
|
||||
The candidate fix explicitly resolves NPM container port 81 to its actual host
|
||||
allocation. Proxy ports never become the admin URL. Missing/malformed admin
|
||||
bindings do not fall back to another service when published bindings are present.
|
||||
Port parsing handles IPv6 authorities and rejects invalid ports. Regressions
|
||||
cover all six three-port permutations, allocated admin ports, IPv4/IPv6 binding
|
||||
strings, missing admin mappings, missing runtime port information, and malformed
|
||||
or UDP bindings. Full backend regression execution is pending for this change.
|
||||
The ISO build is frozen at installer-environment creation; no release was signed
|
||||
or published. Rebuild/revalidate the OTA and ISO with this correction.
|
||||
|
||||
The companion orphan fix worked live: Cuprate UI was automatically removed and
|
||||
all installed app container IDs remained unchanged. One observation helper raced
|
||||
that expected removal between `podman ps` and `inspect`; it now excludes that
|
||||
known orphan before inspection and repeats the stability check. This was a test
|
||||
snapshot race, not another installed-app restart.
|
||||
|
||||
NPM selector final backend gate passed: 1,617 tests, zero failures, four explicitly
|
||||
ignored, through the isolated runner. This includes all new port-selection cases
|
||||
and the existing companion security/configuration and lifecycle regressions.
|
||||
Rebuild the release binary and UI metadata, deploy those exact OTA bytes to both
|
||||
boxes, and require actual kiosk hard-refresh/Launch acceptance before ISO assembly.
|
||||
|
||||
@@ -369,6 +369,7 @@ init()
|
||||
<span class="text-xs text-white/40">September 30, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Fixed Nginx Proxy Manager launch readiness choosing a proxy listener instead of its admin port after container recreation.</p>
|
||||
<p>Network diagnostic failures no longer stop all apps or rebuild shared container networking.</p>
|
||||
<p>Prevented orphaned companion dashboards from repeatedly reinstalling themselves after their backend app was removed.</p>
|
||||
<p>Fixed companion dashboard builds still referencing a retired image registry.</p>
|
||||
|
||||
Reference in New Issue
Block a user