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:
parent
2df19e4899
commit
71e7fd8b20
4 changed files with 473 additions and 84 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue