Skip to content

Commit 8cf0a4e

Browse files
authored
Admin API to get the version of the service (#5102)
2 parents e08ab75 + 0a5d048 commit 8cf0a4e

File tree

9 files changed

+133
-5
lines changed

9 files changed

+133
-5
lines changed

crates/cli/src/app_state.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{convert::Infallible, net::IpAddr, sync::Arc};
99
use axum::extract::{FromRef, FromRequestParts};
1010
use ipnetwork::IpNetwork;
1111
use mas_context::LogContext;
12-
use mas_data_model::{BoxClock, BoxRng, SiteConfig, SystemClock};
12+
use mas_data_model::{AppVersion, BoxClock, BoxRng, SiteConfig, SystemClock};
1313
use mas_handlers::{
1414
ActivityTracker, BoundActivityTracker, CookieManager, ErrorWrapper, GraphQLSchema, Limiter,
1515
MetadataCache, RequesterFingerprint, passwords::PasswordManager,
@@ -27,7 +27,7 @@ use rand::SeedableRng;
2727
use sqlx::PgPool;
2828
use tracing::Instrument;
2929

30-
use crate::telemetry::METER;
30+
use crate::{VERSION, telemetry::METER};
3131

3232
#[derive(Clone)]
3333
pub struct AppState {
@@ -214,6 +214,12 @@ impl FromRef<AppState> for Arc<dyn HomeserverConnection> {
214214
}
215215
}
216216

217+
impl FromRef<AppState> for AppVersion {
218+
fn from_ref(_input: &AppState) -> Self {
219+
AppVersion(VERSION)
220+
}
221+
}
222+
217223
impl FromRequestParts<AppState> for BoxClock {
218224
type Rejection = Infallible;
219225

crates/data-model/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub(crate) mod upstream_oauth2;
1818
pub(crate) mod user_agent;
1919
pub(crate) mod users;
2020
mod utils;
21+
mod version;
2122

2223
/// Error when an invalid state transition is attempted.
2324
#[derive(Debug, Error)]
@@ -57,4 +58,5 @@ pub use self::{
5758
UserRecoveryTicket, UserRegistration, UserRegistrationPassword, UserRegistrationToken,
5859
},
5960
utils::{BoxClock, BoxRng},
61+
version::AppVersion,
6062
};

crates/data-model/src/version.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright 2025 New Vector Ltd.
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
4+
// Please see LICENSE files in the repository root for full details.
5+
6+
/// A structure which holds information about the running version of the app
7+
#[derive(Debug, Clone, Copy)]
8+
pub struct AppVersion(pub &'static str);

crates/handlers/src/admin/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use axum::{
2020
use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
2121
use indexmap::IndexMap;
2222
use mas_axum_utils::InternalError;
23-
use mas_data_model::{BoxRng, SiteConfig};
23+
use mas_data_model::{AppVersion, BoxRng, SiteConfig};
2424
use mas_http::CorsLayerExt;
2525
use mas_matrix::HomeserverConnection;
2626
use mas_policy::PolicyFactory;
@@ -164,6 +164,7 @@ where
164164
UrlBuilder: FromRef<S>,
165165
Arc<PolicyFactory>: FromRef<S>,
166166
SiteConfig: FromRef<S>,
167+
AppVersion: FromRef<S>,
167168
{
168169
// We *always* want to explicitly set the possible responses, beacuse the
169170
// infered ones are not necessarily correct

crates/handlers/src/admin/v1/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use aide::axum::{
1111
routing::{get_with, post_with},
1212
};
1313
use axum::extract::{FromRef, FromRequestParts};
14-
use mas_data_model::{BoxRng, SiteConfig};
14+
use mas_data_model::{AppVersion, BoxRng, SiteConfig};
1515
use mas_matrix::HomeserverConnection;
1616
use mas_policy::PolicyFactory;
1717

@@ -28,13 +28,15 @@ mod user_emails;
2828
mod user_registration_tokens;
2929
mod user_sessions;
3030
mod users;
31+
mod version;
3132

3233
pub fn router<S>() -> ApiRouter<S>
3334
where
3435
S: Clone + Send + Sync + 'static,
3536
Arc<dyn HomeserverConnection>: FromRef<S>,
3637
PasswordManager: FromRef<S>,
3738
SiteConfig: FromRef<S>,
39+
AppVersion: FromRef<S>,
3840
Arc<PolicyFactory>: FromRef<S>,
3941
BoxRng: FromRequestParts<S>,
4042
CallContext: FromRequestParts<S>,
@@ -44,6 +46,10 @@ where
4446
"/site-config",
4547
get_with(self::site_config::handler, self::site_config::doc),
4648
)
49+
.api_route(
50+
"/version",
51+
get_with(self::version::handler, self::version::doc),
52+
)
4753
.api_route(
4854
"/compat-sessions",
4955
get_with(self::compat_sessions::list, self::compat_sessions::list_doc),
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2025 New Vector Ltd.
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
4+
// Please see LICENSE files in the repository root for full details.
5+
6+
use aide::transform::TransformOperation;
7+
use axum::{Json, extract::State};
8+
use mas_data_model::AppVersion;
9+
use schemars::JsonSchema;
10+
use serde::Serialize;
11+
12+
use crate::admin::call_context::CallContext;
13+
14+
#[derive(Serialize, JsonSchema)]
15+
pub struct Version {
16+
/// The semver version of the app
17+
pub version: &'static str,
18+
}
19+
20+
pub fn doc(operation: TransformOperation) -> TransformOperation {
21+
operation
22+
.id("version")
23+
.tag("server")
24+
.summary("Get the version currently running")
25+
.response_with::<200, Json<Version>, _>(|t| t.example(Version { version: "v1.0.0" }))
26+
}
27+
28+
#[tracing::instrument(name = "handler.admin.v1.version", skip_all)]
29+
pub async fn handler(
30+
_: CallContext,
31+
State(AppVersion(version)): State<mas_data_model::AppVersion>,
32+
) -> Json<Version> {
33+
Json(Version { version })
34+
}
35+
36+
#[cfg(test)]
37+
mod tests {
38+
use hyper::{Request, StatusCode};
39+
use insta::assert_json_snapshot;
40+
use sqlx::PgPool;
41+
42+
use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
43+
44+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
45+
async fn test_add_user(pool: PgPool) {
46+
setup();
47+
let mut state = TestState::from_pool(pool).await.unwrap();
48+
let token = state.token_with_scope("urn:mas:admin").await;
49+
50+
let request = Request::get("/api/admin/v1/version").bearer(&token).empty();
51+
52+
let response = state.request(request).await;
53+
54+
assert_eq!(response.status(), StatusCode::OK);
55+
let body: serde_json::Value = response.json();
56+
assert_json_snapshot!(body, @r#"
57+
{
58+
"version": "v0.0.0-test"
59+
}
60+
"#);
61+
}
62+
}

crates/handlers/src/bin/api-schema.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl_from_ref!(mas_keystore::Keystore);
6060
impl_from_ref!(mas_handlers::passwords::PasswordManager);
6161
impl_from_ref!(Arc<mas_policy::PolicyFactory>);
6262
impl_from_ref!(mas_data_model::SiteConfig);
63+
impl_from_ref!(mas_data_model::AppVersion);
6364

6465
fn main() -> Result<(), Box<dyn std::error::Error>> {
6566
let (mut api, _) = mas_handlers::admin_api_router::<DummyState>();

crates/handlers/src/test_utils.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use mas_axum_utils::{
2828
cookies::{CookieJar, CookieManager},
2929
};
3030
use mas_config::RateLimitingConfig;
31-
use mas_data_model::{BoxClock, BoxRng, SiteConfig, clock::MockClock};
31+
use mas_data_model::{AppVersion, BoxClock, BoxRng, SiteConfig, clock::MockClock};
3232
use mas_email::{MailTransport, Mailer};
3333
use mas_i18n::Translator;
3434
use mas_keystore::{Encrypter, JsonWebKey, JsonWebKeySet, Keystore, PrivateKey};
@@ -575,6 +575,12 @@ impl FromRef<TestState> for reqwest::Client {
575575
}
576576
}
577577

578+
impl FromRef<TestState> for AppVersion {
579+
fn from_ref(_input: &TestState) -> Self {
580+
AppVersion("v0.0.0-test")
581+
}
582+
}
583+
578584
impl FromRequestParts<TestState> for ActivityTracker {
579585
type Rejection = Infallible;
580586

docs/api/spec.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,30 @@
5050
}
5151
}
5252
},
53+
"/api/admin/v1/version": {
54+
"get": {
55+
"tags": [
56+
"server"
57+
],
58+
"summary": "Get the version currently running",
59+
"operationId": "version",
60+
"responses": {
61+
"200": {
62+
"description": "",
63+
"content": {
64+
"application/json": {
65+
"schema": {
66+
"$ref": "#/components/schemas/Version"
67+
},
68+
"example": {
69+
"version": "v1.0.0"
70+
}
71+
}
72+
}
73+
}
74+
}
75+
}
76+
},
5377
"/api/admin/v1/compat-sessions": {
5478
"get": {
5579
"tags": [
@@ -3710,6 +3734,18 @@
37103734
}
37113735
}
37123736
},
3737+
"Version": {
3738+
"type": "object",
3739+
"required": [
3740+
"version"
3741+
],
3742+
"properties": {
3743+
"version": {
3744+
"description": "The semver version of the app",
3745+
"type": "string"
3746+
}
3747+
}
3748+
},
37133749
"PaginationParams": {
37143750
"type": "object",
37153751
"properties": {

0 commit comments

Comments
 (0)