Watch
0
0
Fork
You've already forked hyperhive
0

swarmctl: serve login callbacks concurrently, retry a refused token once, test the refresh lock and the socket path

The loopback listener served one connection at a time with no read
timeout, so a connection that never sent a request (a browser preconnect,
any local process) held up the real form_post until the 5-minute wait
ran out. Each connection now runs on its own task and the first final
answer wins. The callback body is capped at 16 KiB; over the cap it reads
as empty and can't complete the login.

A request the gateway answers 401 now triggers one refresh (under the
cache lock, skipped if another process already replaced the refused
token) and one retry; a second 401 says to run `swarmctl login`.

The login cache directory is a `Swarm` field so tests can point it at a
scratch directory. New tests: an idle connection does not block the real
callback; an oversized body is refused without ending the wait; two
concurrent refreshers refresh once and share the token; a refused token
is refreshed once and retried; a second 401 says to log in without a
further retry; with no `--swarm`, the verbs use the controller socket.
This commit is contained in:
atlas 2026-10-12 00:36:48 +02:00 • committed by mara
commit 71e7fd8b20
4 changed files with 473 additions and 84 deletions

View file

@ -83,7 +83,8 @@ impl RemoteArgs {
.ca_file
.clone()
.or_else(|| std::env::var_os("SWARMCTL_CA_FILE").map(PathBuf::from));
url.map(|u| remote::Swarm::parse(&u, ca_file)).transpose()
url.map(|u| remote::Swarm::parse(&u, ca_file, remote::state_dir()?))
.transpose()
}
/// The swarm when one is named, the controller's socket otherwise.
@ -991,6 +992,75 @@ fn write_atomic(path: &Path, contents: &str) -> Result<()> {
mod tests {
use super::*;
/// Root on the controller host, with no `--swarm` and no login: the verbs
/// still go over the unix socket, unchanged by the remote path.
#[test]
fn with_no_swarm_named_the_verbs_use_the_controller_socket() {
assert!(
std::env::var_os("SWARMCTL_SWARM").is_none(),
"this test needs SWARMCTL_SWARM unset"
);
let dir = std::env::temp_dir().join(format!("swarmctl-socket-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("dir");
let socket = dir.join("controller.sock");
let listener = std::os::unix::net::UnixListener::bind(&socket).expect("bind");
listener.set_nonblocking(true).expect("nonblocking");
let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let server_seen = std::sync::Arc::clone(&seen);
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
rt.block_on(async move {
let listener = tokio::net::UnixListener::from_std(listener).expect("listener");
let (stream, _) = listener.accept().await.expect("accept");
let service = hyper::service::service_fn(
move |req: hyper::Request<hyper::body::Incoming>| {
server_seen.lock().expect("seen").push(format!(
"{} {} auth={}",
req.method(),
req.uri(),
req.headers().contains_key(hyper::header::AUTHORIZATION)
));
async {
Ok::<_, std::convert::Infallible>(hyper::Response::new(
http_body_util::Full::new(bytes::Bytes::from_static(b"[]")),
))
}
},
);
let _ = hyper::server::conn::http1::Builder::new()
.serve_connection(hyper_util::rt::TokioIo::new(stream), service)
.await;
});
});
let cli = Cli::try_parse_from([
"swarmctl",
"schedule",
"list",
"--controller-socket",
socket.to_str().expect("utf-8"),
])
.expect("parses");
let Verb::Schedule {
command: ScheduleVerb::List(args),
} = cli.command
else {
panic!("parsed as another verb");
};
let target = args.socket.resolve(&cli.remote).expect("resolves");
assert!(matches!(&target, Target::Socket(p) if *p == socket));
schedule::list(&target, None).expect("lists over the socket");
assert_eq!(
*seen.lock().expect("seen"),
["GET /api/schedules auth=false"]
);
fs::remove_dir_all(&dir).expect("cleanup");
}
/// clap's own consistency checks over the whole tree — a duplicated
/// long flag or a malformed `value_name` panics at parse time in
/// production and is otherwise only found by running the binary.