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)
|
## 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.
|
- 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.
|
- Prevented orphaned companion dashboards from repeatedly reinstalling themselves after their backend app was removed.
|
||||||
- Fixed companion dashboard builds still referencing a retired image registry.
|
- 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;
|
let mut first_candidate = None;
|
||||||
for port_str in ports {
|
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"
|
// 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;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(port_part) = public_part.split(':').nth(1) else {
|
let Some((_, port_part)) = public_part.rsplit_once(':') else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// Extract just the first port if it's a range (e.g., "18443-18444" -> "18443")
|
// 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 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);
|
let candidate = format!("http://localhost:{}", host_port);
|
||||||
if first_candidate.is_none() {
|
if first_candidate.is_none() {
|
||||||
first_candidate = Some(candidate.clone());
|
first_candidate = Some(candidate.clone());
|
||||||
@@ -1113,6 +1119,29 @@ fn package_launch_candidate(
|
|||||||
if let Some(companion) = companion_lan_address(app_id) {
|
if let Some(companion) = companion_lan_address(app_id) {
|
||||||
return Some(companion);
|
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) {
|
if uses_allocated_launch_port(app_id) {
|
||||||
extract_lan_address(ports).or(known)
|
extract_lan_address(ports).or(known)
|
||||||
} else {
|
} 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]
|
#[test]
|
||||||
fn skips_ssh_port_when_web_port_is_published() {
|
fn skips_ssh_port_when_web_port_is_published() {
|
||||||
// gitea: SSH published before the web port, in podman's list order.
|
// 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.
|
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 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.
|
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>
|
<span class="text-xs text-white/40">September 30, 2026</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
<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>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>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>
|
<p>Fixed companion dashboard builds still referencing a retired image registry.</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user