slot registration API skeleton

This commit is contained in:
Thomas Weinhold 2026-02-11 01:00:38 +01:00
commit 93d8da878d
8 changed files with 183 additions and 28 deletions

View file

@ -1,5 +1,4 @@
use std::fs;
use std::path::Path;
use std::process::Command;
fn main() {
@ -32,13 +31,12 @@ fn main() {
"unknown".to_string()
};
// Write a Rust file with the version constant
let out_dir = std::env::var("OUT_DIR").unwrap();
fs::write(
format!("{}/global_config.rs", out_dir),
format!("{}/version_info.rs", out_dir),
format!("pub const GLOBAL_CONFIG_VERSION: &str = \"{}\";\n\
pub const GLOBAL_CONFIG_BRANCH: &str = \"{}\";",
version, branch),
).expect("Failed to write global_config.rs");
version, branch),
).expect("Failed to write version_info.rs");
}

View file

@ -1,5 +1,7 @@
use std::path::PathBuf;
include!(concat!(env!("OUT_DIR"), "/version_info.rs"));
#[derive(Clone)]
pub struct SsloAppState<T> {
database_path: PathBuf,
@ -29,4 +31,17 @@ where T: Clone
pub fn substate_mut(&mut self) -> &mut T {
&mut self.sub_state
}
}
pub trait SsloAppStateRevisionInfo {
/// The latest version number from a git tag
fn version(&self) -> &str;
/// The name of the git repository branch during compile
fn branch(&self) -> &str;
}
impl<T> SsloAppStateRevisionInfo for SsloAppState<T> {
fn version(&self) -> &str { GLOBAL_CONFIG_VERSION }
fn branch(&self) -> &str { GLOBAL_CONFIG_BRANCH }
}

View file

@ -1,5 +1,16 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::app_state::SsloAppStateRevisionInfo;
/// This is the major version of the REST API
/// With any compatibility breaking updates, this needs to be incremented
const SLOT_REST_API_VERSION : u16 = 1;
pub enum ApiCompatibility {
SlotOutdated,
LeagueOutdated,
Compatible,
}
/// General slot information
#[derive(ToSchema, Serialize, Deserialize)]
@ -8,38 +19,86 @@ pub struct SlotInfoResponse {
/// The name of the slot
pub name: String,
/// The version of the slot
/// The version identifier of the slot
pub version: String,
/// The API version that is expected by the slot
pub api_version: u16,
/// The repository branch that was used for compilation
pub branch: String,
}
impl SlotInfoResponse{
pub fn new(app_state: &impl SsloAppStateRevisionInfo, name: String) -> Self {
Self {
name,
version: app_state.version().to_string(),
api_version: SLOT_REST_API_VERSION,
branch: app_state.branch().to_string(),
}
}
}
/// Request of a league to register on a slot
#[derive(ToSchema, Serialize, Deserialize)]
pub struct SlotRegisterRequest {
/// Full qualified domain name of the league, including port (e.g. sslo.my-league.org:443)
/// Full qualified domain name of the league,
/// including UDP port (e.g. sslo.my-league.org:8000)
pub fqdn: String,
/// The version number of the league
/// This is the requested ID of the slot in the league,
/// It will be sent with all UDP requests from the slot to the league
pub slot_id: u16,
/// The version identifier of the league
pub version: String,
/// The API version that is expected by the league
pub api_version: u16,
}
impl SlotRegisterRequest {
pub fn new(app_state: &impl SsloAppStateRevisionInfo,
fqdn: String,
slot_id: u16,
) -> Self { Self {
fqdn,
slot_id,
version: app_state.version().to_string(),
api_version: SLOT_REST_API_VERSION,
} }
pub fn compatibility(&self) -> ApiCompatibility {
if self.api_version < SLOT_REST_API_VERSION { ApiCompatibility::LeagueOutdated }
else if self.api_version > SLOT_REST_API_VERSION { ApiCompatibility::SlotOutdated }
else { ApiCompatibility::Compatible }
}
}
/// Request of a league to register on a slot
#[derive(ToSchema, Serialize, Deserialize)]
pub struct SlotRegisterResponse {
/// The Name of the slot
/// The name of the slot
pub name: String,
/// The version of the slot
/// The version identifier of the slot
pub version: String,
/// The API version that is expected by the slot
pub api_version: u16,
}
impl SlotRegisterResponse {
pub fn new(app_state: &impl SsloAppStateRevisionInfo, name: String) -> Self { Self {
name,
version: app_state.version().to_string(),
api_version: SLOT_REST_API_VERSION,
} }
/// Continuous status reporting from slot to registered league
#[derive(ToSchema, Serialize)]
pub struct LeagueRequestSlotStatus {
pub fn compatibility(&self) -> ApiCompatibility {
if self.api_version < SLOT_REST_API_VERSION { ApiCompatibility::SlotOutdated }
else if self.api_version > SLOT_REST_API_VERSION { ApiCompatibility::LeagueOutdated }
else { ApiCompatibility::Compatible }
}
}

View file

@ -1,4 +1,5 @@
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use axum_server::tls_rustls::RustlsConfig;
use sslo_lib::app_state::SsloAppState;
use sslo_lib::error::SsloError;
@ -13,15 +14,16 @@ pub type AppState = SsloAppState<SlotAppState>;
pub struct SlotAppState {
pub shutdown_control: ShutdownControl,
pub bearer_token_encrypted: String,
pub config: UserConfig,
pub user_config: UserConfig,
pub database_dir: PathBuf,
registered_league: Arc<RwLock<Option<(String, u16)>>>, // (fqdn, id)
}
impl SlotAppState {
pub async fn new(database_dir: PathBuf) -> Result<AppState, SsloError> {
// parse config
// parse user config
let config = UserConfig::load_or_create(database_dir.join("config.toml"))?;
// shutdown controller
@ -40,8 +42,9 @@ impl SlotAppState {
let app_state = Self {
shutdown_control,
bearer_token_encrypted,
config,
user_config: config,
database_dir: database_dir.clone(),
registered_league: Arc::new(RwLock::new(None)),
};
Ok(SsloAppState::new(database_dir, app_state))
}
@ -78,4 +81,18 @@ impl SlotAppState {
}
}
}
pub fn register_league(&self, fqdn: String, slot_id: u16) {
let mut guard = self.registered_league.write().unwrap_or_else(|poisoned_guard| {
poisoned_guard.into_inner()
});
*guard = Some((fqdn, slot_id));
}
pub fn unregister_league(&self) {
let mut guard = self.registered_league.write().unwrap_or_else(|poisoned_guard| {
poisoned_guard.into_inner()
});
*guard = None;
}
}

View file

@ -1,4 +1,5 @@
mod info;
mod register;
use crate::app_state::AppState;
@ -21,6 +22,8 @@ use sslo_lib::token::Token;
pub fn create_router(app_state: AppState) -> Router {
let (router, api) = OpenApiRouter::<AppState>::with_openapi(ApiDoc::openapi())
.routes(routes!(info::handler))
.routes(routes!(register::handler_post))
.routes(routes!(register::handler_delete))
.split_for_parts();
// bearer authorization middleware

View file

@ -5,8 +5,6 @@ use axum::response::{IntoResponse, Response};
use sslo_lib::slot_rest_api::SlotInfoResponse;
use crate::app_state::AppState;
include!(concat!(env!("OUT_DIR"), "/global_config.rs"));
#[utoipa::path(
get,
@ -19,12 +17,7 @@ include!(concat!(env!("OUT_DIR"), "/global_config.rs"));
)
)]
pub async fn handler(State(app_state): State<AppState>) -> Response {
let res = SlotInfoResponse {
name: app_state.substate().config.name.clone(),
version: GLOBAL_CONFIG_VERSION.to_string(),
branch: GLOBAL_CONFIG_BRANCH.to_string(),
};
let name = app_state.substate().user_config.name.clone();
let res = SlotInfoResponse::new(&app_state, name);
(StatusCode::OK, Json(res)).into_response()
}

View file

@ -0,0 +1,70 @@
use axum::extract::State;
use axum::http::StatusCode;
use axum::Json;
use axum::response::{IntoResponse, Response};
use sslo_lib::app_state::SsloAppStateRevisionInfo;
use sslo_lib::slot_rest_api::{ApiCompatibility, SlotRegisterRequest, SlotRegisterResponse};
use crate::app_state::AppState;
#[utoipa::path(
post,
tag="League Interface",
path="/register",
summary="register a league",
description="Registers a league to this slot. An already existing league registration will be dropped.",
request_body=SlotRegisterRequest,
responses(
(status=200, body=SlotRegisterResponse),
(status=406, body=SlotRegisterResponse),
)
)]
pub async fn handler_post(State(app_state): State<AppState>,
Json(request): Json<SlotRegisterRequest>,
) -> Response {
// prepare response
let name = app_state.substate().user_config.name.clone();
let res = SlotRegisterResponse::new(&app_state, name);
// check compatibility
let status = match request.compatibility() {
ApiCompatibility::SlotOutdated => {
log::warn!("Reject league registration request, because slot is outdated! League version={} slot version={}",
request.version, app_state.version(),
);
StatusCode::NOT_ACCEPTABLE
},
ApiCompatibility::LeagueOutdated => {
log::warn!("Reject league registration request, because league is outdated! League version={} slot version={}",
request.version, app_state.version(),
);
StatusCode::NOT_ACCEPTABLE
},
ApiCompatibility::Compatible => {
log::info!("League registration request accepted. League '{}' with version={}",
request.fqdn, request.version,
);
app_state.substate().register_league(request.fqdn, request.slot_id);
StatusCode::OK
},
};
// send response
(status, Json(res)).into_response()
}
#[utoipa::path(
delete,
tag="League Interface",
path="/register",
summary="unregister any league",
description="Unregister the current league from this slot. This has no effect when no league is connected.",
responses(
(status=204),
)
)]
pub async fn handler_delete(State(app_state): State<AppState>) -> Response {
app_state.substate().unregister_league();
StatusCode::NO_CONTENT.into_response()
}

View file

@ -35,8 +35,8 @@ async fn main() {
let axum_app = http::create_router(app_state.clone());
let axum_handle = axum_server::Handle::new();
app_state.substate_mut().shutdown_control.add_axum_server_handle(axum_handle.clone());
let socket_addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, app_state.substate().config.start_port));
if app_state.substate().config.use_embedded_tls {
let socket_addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, app_state.substate().user_config.start_port));
if app_state.substate().user_config.use_embedded_tls {
if let Ok(tls_cfg) = app_state.substate().get_rustls_config().await {
match axum_server::bind_rustls(socket_addr, tls_cfg)
.handle(axum_handle)