diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 4f834a7351..aa76a0f561 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2973,30 +2973,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.86" @@ -4308,9 +4284,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "4.2.3" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" +checksum = "514a48569e4e21c86d0b84b5612b5e73c0b2cf09db63260134ba426d4e8ea714" dependencies = [ "indexmap 2.6.0", "serde", @@ -4320,11 +4296,10 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "4.3.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bf0e16c02bc4bf5322ab65f10ab1149bdbcaa782cba66dc7057370a3f8190be" +checksum = "5629efe65599d0ccd5d493688cbf6e03aa7c1da07fe59ff97cf5977ed0637f66" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", "regex", diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index f9278389ca..c004dd590b 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -21,16 +21,16 @@ thiserror = "1.0.64" tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1.16" url = "2.5.2" -utoipa = "4.2.3" +utoipa = "5.2.0" zbus = { version = "5", default-features = false, features = ["tokio"] } # Needed to define curl error in profile errors curl = { version = "0.4.47", features = ["protocol-ftp"] } jsonwebtoken = "9.3.0" chrono = { version = "0.4.38", default-features = false, features = [ - "now", - "std", - "alloc", - "clock", + "now", + "std", + "alloc", + "clock", ] } home = "0.5.9" strum = { version = "0.26.3", features = ["derive"] } diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs index 13fd52c99a..d767bef78f 100644 --- a/rust/agama-lib/src/lib.rs +++ b/rust/agama-lib/src/lib.rs @@ -63,12 +63,14 @@ pub mod progress; pub mod proxies; mod store; pub use store::Store; -use zbus::conn::Builder; +pub mod openapi; pub mod questions; pub mod scripts; pub mod transfer; + use crate::error::ServiceError; use reqwest::{header, Client}; +use zbus::conn::Builder; const ADDRESS: &str = "unix:path=/run/agama/bus"; diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index 834b6425f6..f329085d46 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -21,6 +21,7 @@ //! Representation of the network settings use super::types::{DeviceState, DeviceType, Status}; +use crate::openapi::schemas; use cidr::IpInet; use serde::{Deserialize, Serialize}; use std::default::Default; @@ -176,18 +177,22 @@ pub struct NetworkConnection { pub method4: Option, /// Gateway IP address for the IPv4 connection #[serde(skip_serializing_if = "Option::is_none")] + #[schema(schema_with = schemas::ip_addr_ref)] pub gateway4: Option, /// IPv6 method used for the network connection #[serde(skip_serializing_if = "Option::is_none")] pub method6: Option, /// Gateway IP address for the IPv6 connection #[serde(skip_serializing_if = "Option::is_none")] + #[schema(schema_with = schemas::ip_addr_ref)] pub gateway6: Option, /// List of assigned IP addresses #[serde(skip_serializing_if = "Vec::is_empty", default)] + #[schema(schema_with = schemas::ip_inet_array)] pub addresses: Vec, /// List of DNS server IP addresses #[serde(skip_serializing_if = "Vec::is_empty", default)] + #[schema(schema_with = schemas::ip_addr_array)] pub nameservers: Vec, /// List of search domains for DNS resolution #[serde(skip_serializing_if = "Vec::is_empty", default)] diff --git a/rust/agama-lib/src/openapi.rs b/rust/agama-lib/src/openapi.rs new file mode 100644 index 0000000000..79cfe6acae --- /dev/null +++ b/rust/agama-lib/src/openapi.rs @@ -0,0 +1,79 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +pub mod schemas { + use serde_json::json; + use utoipa::openapi::{ + schema::{self, SchemaType}, + Object, ObjectBuilder, Type, + }; + + /// Returns the IPAddr schema. + pub fn ip_addr() -> Object { + ObjectBuilder::new() + .schema_type(SchemaType::new(Type::String)) + .description(Some("An IP address (IPv4 or IPv6)".to_string())) + .examples(vec![json!("192.168.1.100")]) + .build() + } + + /// Reference to IPAddr schema reference. + pub fn ip_addr_ref() -> schema::Ref { + schema::Ref::from_schema_name("IpAddr") + } + + /// Array of IPAddr schema references. + pub fn ip_addr_array() -> schema::Array { + schema::Array::new(ip_addr_ref()) + } + + /// Returns the IpInet schema. + pub fn ip_inet() -> Object { + ObjectBuilder::new() + .schema_type(SchemaType::new(Type::String)) + .description(Some( + "An IP address (IPv4 or IPv6) including the prefix".to_string(), + )) + .examples(vec![json!("192.168.1.254/24")]) + .build() + } + + /// Reference to IpInet schema reference. + pub fn ip_inet_ref() -> schema::Ref { + schema::Ref::from_schema_name("IpInet") + } + + /// Array of IpInet schema references. + pub fn ip_inet_array() -> schema::Array { + schema::Array::new(ip_inet_ref()) + } + + /// MAC address 6 schema. + pub fn mac_addr6() -> Object { + ObjectBuilder::new() + .description(Some("MAC address in EUI-48 format")) + .build() + } + + /// MAC address 6 schema reference. + pub fn mac_addr6_ref() -> schema::Ref { + schema::Ref::from_schema_name("macaddr.MacAddr6") + } +} diff --git a/rust/agama-lib/src/scripts/model.rs b/rust/agama-lib/src/scripts/model.rs index 8a2b3ff193..f32769b78a 100644 --- a/rust/agama-lib/src/scripts/model.rs +++ b/rust/agama-lib/src/scripts/model.rs @@ -32,7 +32,9 @@ use crate::transfer::Transfer; use super::ScriptError; -#[derive(Debug, Clone, Copy, PartialEq, strum::Display, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, strum::Display, Serialize, Deserialize, utoipa::ToSchema, +)] #[strum(serialize_all = "camelCase")] #[serde(rename_all = "camelCase")] pub enum ScriptsGroup { @@ -40,7 +42,7 @@ pub enum ScriptsGroup { Post, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] #[serde(untagged)] pub enum ScriptSource { /// Script's body. @@ -50,7 +52,7 @@ pub enum ScriptSource { } /// Represents a script to run as part of the installation process. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] pub struct Script { /// Script's name. pub name: String, diff --git a/rust/agama-lib/src/storage/settings.rs b/rust/agama-lib/src/storage/settings.rs index ac894e252f..67068e2a9c 100644 --- a/rust/agama-lib/src/storage/settings.rs +++ b/rust/agama-lib/src/storage/settings.rs @@ -25,14 +25,16 @@ use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; /// Storage settings for installation -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct StorageSettings { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[schema(value_type = String)] pub storage: Option>, #[serde(default, rename = "legacyAutoyastStorage")] #[serde(skip_serializing_if = "Option::is_none")] + #[schema(value_type = String)] pub storage_autoyast: Option>, } diff --git a/rust/agama-locale-data/Cargo.toml b/rust/agama-locale-data/Cargo.toml index c95c3214cd..c1c8c2b494 100644 --- a/rust/agama-locale-data/Cargo.toml +++ b/rust/agama-locale-data/Cargo.toml @@ -13,4 +13,4 @@ flate2 = "1.0.34" chrono-tz = "0.8.6" regex = "1" thiserror = "1.0.64" -utoipa = "4.2.3" +utoipa = "5.2.0" diff --git a/rust/agama-server/Cargo.toml b/rust/agama-server/Cargo.toml index 5004271e50..efde389cf8 100644 --- a/rust/agama-server/Cargo.toml +++ b/rust/agama-server/Cargo.toml @@ -30,7 +30,7 @@ tracing-journald = "0.3.0" tracing = "0.1.40" clap = { version = "4.5.19", features = ["derive", "wrap_help"] } tower = { version = "0.4.13", features = ["util"] } -utoipa = { version = "4.2.0", features = ["axum_extras", "uuid"] } +utoipa = { version = "5.2.0", features = ["axum_extras", "uuid"] } config = "0.14.0" rand = "0.8.5" axum-extra = { version = "0.9.4", features = ["cookie", "typed-header"] } @@ -43,7 +43,7 @@ hyper = "1.4.1" hyper-util = "0.1.9" tokio-openssl = "0.6.5" futures-util = { version = "0.3.30", default-features = false, features = [ - "alloc", + "alloc", ] } libsystemd = "0.7.0" subprocess = "0.2.9" diff --git a/rust/agama-server/src/l10n/web.rs b/rust/agama-server/src/l10n/web.rs index dd351bf9cc..193ef0de6d 100644 --- a/rust/agama-server/src/l10n/web.rs +++ b/rust/agama-server/src/l10n/web.rs @@ -77,9 +77,14 @@ pub async fn l10n_service( Ok(router) } -#[utoipa::path(get, path = "/l10n/locales", responses( - (status = 200, description = "List of known locales", body = Vec) -))] +#[utoipa::path( + get, + path = "/locales", + context_path = "/api/l10n", + responses( + (status = 200, description = "List of known locales", body = Vec) + ) +)] async fn locales(State(state): State>) -> Json> { let data = state.locale.read().await; let locales = data.locales_db.entries().to_vec(); diff --git a/rust/agama-server/src/manager/web.rs b/rust/agama-server/src/manager/web.rs index 052336bfb7..ff6009440a 100644 --- a/rust/agama-server/src/manager/web.rs +++ b/rust/agama-server/src/manager/web.rs @@ -27,7 +27,7 @@ use agama_lib::{ error::ServiceError, - logs::{list as list_logs, store as store_logs, LogsLists, DEFAULT_COMPRESSION}, + logs, manager::{InstallationPhase, InstallerStatus, ManagerClient}, proxies::Manager1Proxy, }; @@ -215,20 +215,23 @@ async fn installer_status( fn logs_router() -> Router> { Router::new() .route("/store", get(download_logs)) - .route("/list", get(show_logs)) + .route("/list", get(list_logs)) } -#[utoipa::path(get, path = "/manager/logs/store", responses( - (status = 200, description = "Compressed Agama logs", content_type="application/octet-stream"), - (status = 500, description = "Cannot collect the logs"), - (status = 507, description = "Server is probably out of space"), -))] - +#[utoipa::path(get, + path = "/logs/store", + context_path = "/api/manager", + responses( + (status = 200, description = "Compressed Agama logs", content_type="application/octet-stream"), + (status = 500, description = "Cannot collect the logs"), + (status = 507, description = "Server is probably out of space"), + ) +)] async fn download_logs() -> impl IntoResponse { let mut headers = HeaderMap::new(); let err_response = (headers.clone(), Body::empty()); - match store_logs() { + match logs::store() { Ok(path) => { if let Ok(file) = tokio::fs::File::open(path.clone()).await { let stream = ReaderStream::new(file); @@ -248,7 +251,7 @@ async fn download_logs() -> impl IntoResponse { ); headers.insert( header::CONTENT_ENCODING, - HeaderValue::from_static(DEFAULT_COMPRESSION.1), + HeaderValue::from_static(logs::DEFAULT_COMPRESSION.1), ); (StatusCode::OK, (headers, body)) @@ -259,9 +262,14 @@ async fn download_logs() -> impl IntoResponse { Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, err_response), } } -#[utoipa::path(get, path = "/manager/logs/list", responses( - (status = 200, description = "Lists of collected logs", body = LogsLists) -))] -pub async fn show_logs() -> Json { - Json(list_logs()) + +#[utoipa::path(get, + path = "/logs/list", + context_path = "/api/manager", + responses( + (status = 200, description = "Lists of collected logs", body = logs::LogsLists) + ) +)] +pub async fn list_logs() -> Json { + Json(logs::list()) } diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index b802d3b9ed..400a5ca23c 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -27,6 +27,7 @@ use agama_lib::network::settings::{ BondSettings, IEEE8021XSettings, NetworkConnection, WirelessSettings, }; use agama_lib::network::types::{BondMode, DeviceState, DeviceType, Status, SSID}; +use agama_lib::openapi::schemas; use cidr::IpInet; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none, DisplayFromStr}; @@ -737,6 +738,7 @@ pub struct InvalidMacAddress(String); #[derive(Debug, Default, Clone, PartialEq, Serialize, utoipa::ToSchema)] pub enum MacAddress { + #[schema(value_type = String, format = "MAC address in EUI-48 format")] MacAddress(macaddr::MacAddr6), Preserve, Permanent, @@ -802,13 +804,17 @@ pub struct IpConfig { pub method4: Ipv4Method, pub method6: Ipv6Method, #[serde(skip_serializing_if = "Vec::is_empty")] + #[schema(schema_with = schemas::ip_inet_array)] pub addresses: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] + #[schema(schema_with = schemas::ip_addr_array)] pub nameservers: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] pub dns_searchlist: Vec, pub ignore_auto_dns: bool, + #[schema(schema_with = schemas::ip_addr)] pub gateway4: Option, + #[schema(schema_with = schemas::ip_addr)] pub gateway6: Option, #[serde(skip_serializing_if = "Vec::is_empty")] pub routes4: Vec, @@ -920,8 +926,10 @@ impl From for zbus::fdo::Error { #[derive(Debug, PartialEq, Clone, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct IpRoute { + #[schema(schema_with = schemas::ip_inet_ref)] pub destination: IpInet, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(schema_with = schemas::ip_addr)] pub next_hop: Option, #[serde(skip_serializing_if = "Option::is_none")] pub metric: Option, @@ -1000,6 +1008,7 @@ pub struct WirelessConfig { pub band: Option, pub channel: u32, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(schema_with = schemas::mac_addr6)] pub bssid: Option, #[serde(skip_serializing_if = "Option::is_none")] pub wep_security: Option, diff --git a/rust/agama-server/src/network/web.rs b/rust/agama-server/src/network/web.rs index 67e5463908..5c0218fd8a 100644 --- a/rust/agama-server/src/network/web.rs +++ b/rust/agama-server/src/network/web.rs @@ -134,7 +134,7 @@ pub async fn network_service( path = "/state", context_path = "/api/network", responses( - (status = 200, description = "Get general network config", body = GenereralState) + (status = 200, description = "Get general network config", body = GeneralState) ) )] async fn general_state( @@ -149,7 +149,7 @@ async fn general_state( path = "/state", context_path = "/api/network", responses( - (status = 200, description = "Update general network config", body = GenereralState) + (status = 200, description = "Update general network config", body = GeneralState) ) )] async fn update_general_state( @@ -243,7 +243,8 @@ async fn add_connection( #[utoipa::path( get, - path = "/network/connections/:id", + path = "/connections/:id", + context_path = "/api/network", responses( (status = 200, description = "Get connection given by its ID", body = NetworkConnection) ) diff --git a/rust/agama-server/src/questions/web.rs b/rust/agama-server/src/questions/web.rs index b51d5d2d69..b66c6534c8 100644 --- a/rust/agama-server/src/questions/web.rs +++ b/rust/agama-server/src/questions/web.rs @@ -285,10 +285,15 @@ pub async fn questions_stream( /// Returns the list of questions that waits for answer. /// /// * `state`: service state. -#[utoipa::path(get, path = "/questions", responses( - (status = 200, description = "List of open questions", body = Vec), - (status = 400, description = "The D-Bus service could not perform the action") -))] +#[utoipa::path( + get, + path = "/", + context_path = "/api/questions", + responses( + (status = 200, description = "List of open questions", body = Vec), + (status = 400, description = "The D-Bus service could not perform the action") + ) +)] async fn list_questions( State(state): State>, ) -> Result>, Error> { @@ -299,11 +304,16 @@ async fn list_questions( /// /// * `state`: service state. /// * `questions_id`: id of question -#[utoipa::path(put, path = "/questions/:id/answer", responses( - (status = 200, description = "Answer"), - (status = 400, description = "The D-Bus service could not perform the action"), - (status = 404, description = "Answer was not yet provided"), -))] +#[utoipa::path( + put, + path = "/:id/answer", + context_path = "/api/questions", + responses( + (status = 200, description = "Answer"), + (status = 400, description = "The D-Bus service could not perform the action"), + (status = 404, description = "Answer was not yet provided"), + ) +)] async fn get_answer( State(state): State>, Path(question_id): Path, @@ -321,10 +331,15 @@ async fn get_answer( /// * `state`: service state. /// * `questions_id`: id of question /// * `answer`: struct with answer and possible other data needed for answer like password -#[utoipa::path(put, path = "/questions/:id/answer", responses( - (status = 200, description = "answer question"), - (status = 400, description = "The D-Bus service could not perform the action") -))] +#[utoipa::path( + put, + path = "/:id/answer", + context_path = "/api/questions", + responses( + (status = 200, description = "answer question"), + (status = 400, description = "The D-Bus service could not perform the action") + ) +)] async fn answer_question( State(state): State>, Path(question_id): Path, @@ -338,10 +353,15 @@ async fn answer_question( /// /// * `state`: service state. /// * `questions_id`: id of question -#[utoipa::path(delete, path = "/questions/:id", responses( - (status = 200, description = "question deleted"), - (status = 400, description = "The D-Bus service could not perform the action") -))] +#[utoipa::path( + delete, + path = "/:id", + context_path = "/api/questions", + responses( + (status = 200, description = "question deleted"), + (status = 400, description = "The D-Bus service could not perform the action") + ) +)] async fn delete_question( State(state): State>, Path(question_id): Path, @@ -354,10 +374,15 @@ async fn delete_question( /// /// * `state`: service state. /// * `question`: struct with question where id of question is ignored and will be assigned -#[utoipa::path(post, path = "/questions", responses( - (status = 200, description = "answer question"), - (status = 400, description = "The D-Bus service could not perform the action") -))] +#[utoipa::path( + post, + path = "/", + context_path = "/api/questions", + responses( + (status = 200, description = "answer question"), + (status = 400, description = "The D-Bus service could not perform the action") + ) +)] async fn create_question( State(state): State>, Json(question): Json, diff --git a/rust/agama-server/src/scripts/web.rs b/rust/agama-server/src/scripts/web.rs index 7625410bcd..9fa8f70462 100644 --- a/rust/agama-server/src/scripts/web.rs +++ b/rust/agama-server/src/scripts/web.rs @@ -71,7 +71,7 @@ pub async fn scripts_service() -> Result { post, path = "/", context_path = "/api/scripts", - request_body(content = ScriptConfig, description = "Script definition"), + request_body(content = [Script], description = "Script definition"), responses( (status = 200, description = "The script was added.") ) diff --git a/rust/agama-server/src/storage/web/dasd.rs b/rust/agama-server/src/storage/web/dasd.rs index 086b767b58..c681f3489d 100644 --- a/rust/agama-server/src/storage/web/dasd.rs +++ b/rust/agama-server/src/storage/web/dasd.rs @@ -85,6 +85,7 @@ pub async fn dasd_service(dbus: &zbus::Connection) -> Result, Servi get, path="/supported", context_path="/api/storage/dasd", + operation_id = "dasd_supported", responses( (status = OK, description = "Returns whether DASD technology is supported") ) @@ -118,6 +119,7 @@ async fn devices(State(state): State>) -> Result, } diff --git a/rust/agama-server/src/storage/web/zfcp.rs b/rust/agama-server/src/storage/web/zfcp.rs index f29346a844..c1b3569ce6 100644 --- a/rust/agama-server/src/storage/web/zfcp.rs +++ b/rust/agama-server/src/storage/web/zfcp.rs @@ -103,6 +103,7 @@ pub async fn zfcp_service(dbus: &zbus::Connection) -> Result, Servi get, path="/supported", context_path="/api/storage/zfcp", + operation_id = "zfcp_supported", responses( (status = OK, description = "Returns whether zFCP technology is supported") ) @@ -276,6 +277,7 @@ async fn deactivate_disk( post, path="/probe", context_path="/api/storage/zfcp", + operation_id = "zfcp_probe", responses( (status = OK, description = "The probing process ran successfully") ) diff --git a/rust/agama-server/src/users/web.rs b/rust/agama-server/src/users/web.rs index 4394c2b605..b4300b1920 100644 --- a/rust/agama-server/src/users/web.rs +++ b/rust/agama-server/src/users/web.rs @@ -164,20 +164,30 @@ pub async fn users_service(dbus: zbus::Connection) -> Result>) -> Result<(), Error> { state.users.remove_first_user().await?; Ok(()) } -#[utoipa::path(put, path = "/users/first", responses( - (status = 200, description = "Sets the first user"), - (status = 400, description = "The D-Bus service could not perform the action"), - (status = 422, description = "Invalid first user. Details are in body", body = Vec), -))] +#[utoipa::path( + put, + path = "/first", + context_path = "/api/users", + responses( + (status = 200, description = "Sets the first user"), + (status = 400, description = "The D-Bus service could not perform the action"), + (status = 422, description = "Invalid first user. Details are in body", body = Vec), + ) +)] async fn set_first_user( State(state): State>, Json(config): Json, @@ -194,18 +204,28 @@ async fn set_first_user( Ok((status, Json(issues).into_response())) } -#[utoipa::path(get, path = "/users/first", responses( - (status = 200, description = "Configuration for the first user", body = FirstUser), - (status = 400, description = "The D-Bus service could not perform the action"), -))] +#[utoipa::path( + get, + path = "/first", + context_path = "/api/users", + responses( + (status = 200, description = "Configuration for the first user", body = FirstUser), + (status = 400, description = "The D-Bus service could not perform the action"), + ) +)] async fn get_user_config(State(state): State>) -> Result, Error> { Ok(Json(state.users.first_user().await?)) } -#[utoipa::path(patch, path = "/users/root", responses( - (status = 200, description = "Root configuration is modified", body = RootPatchSettings), - (status = 400, description = "The D-Bus service could not perform the action"), -))] +#[utoipa::path( + patch, + path = "/root", + context_path = "/api/users", + responses( + (status = 200, description = "Root configuration is modified", body = RootPatchSettings), + (status = 400, description = "The D-Bus service could not perform the action"), + ) +)] async fn patch_root( State(state): State>, Json(config): Json, @@ -232,10 +252,15 @@ async fn patch_root( Ok(Json(retcode)) } -#[utoipa::path(get, path = "/users/root", responses( - (status = 200, description = "Configuration for the root user", body = RootConfig), - (status = 400, description = "The D-Bus service could not perform the action"), -))] +#[utoipa::path( + get, + path = "/root", + context_path = "/api/users", + responses( + (status = 200, description = "Configuration for the root user", body = RootConfig), + (status = 400, description = "The D-Bus service could not perform the action"), + ) +)] async fn get_root_config(State(state): State>) -> Result, Error> { let password = state.users.is_root_password().await?; let sshkey = state.users.root_ssh_key().await?; diff --git a/rust/agama-server/src/web/docs/issues.rs b/rust/agama-server/src/web/docs/issues.rs new file mode 100644 index 0000000000..201aeea35b --- /dev/null +++ b/rust/agama-server/src/web/docs/issues.rs @@ -0,0 +1,43 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; + +use super::ApiDocBuilder; + +pub struct IssuesApiDocBuilder; + +impl ApiDocBuilder for IssuesApiDocBuilder { + fn title(&self) -> String { + "Issues HTTP API".to_string() + } + + fn paths(&self) -> Paths { + PathsBuilder::new() + .path_from::() + .build() + } + + fn components(&self) -> Components { + ComponentsBuilder::new() + .schema_from::() + .build() + } +} diff --git a/rust/agama-server/src/web/docs/l10n.rs b/rust/agama-server/src/web/docs/l10n.rs index a6a17ead4f..8f47138c4b 100644 --- a/rust/agama-server/src/web/docs/l10n.rs +++ b/rust/agama-server/src/web/docs/l10n.rs @@ -1,3 +1,23 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; use super::ApiDocBuilder; diff --git a/rust/agama-server/src/web/docs/manager.rs b/rust/agama-server/src/web/docs/manager.rs index 089107827f..57e1ae5c1e 100644 --- a/rust/agama-server/src/web/docs/manager.rs +++ b/rust/agama-server/src/web/docs/manager.rs @@ -1,3 +1,23 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + use utoipa::openapi::{ComponentsBuilder, PathsBuilder}; use super::ApiDocBuilder; @@ -11,9 +31,11 @@ impl ApiDocBuilder for ManagerApiDocBuilder { fn paths(&self) -> utoipa::openapi::Paths { PathsBuilder::new() + .path_from::() .path_from::() .path_from::() .path_from::() + .path_from::() .path_from::() .build() } @@ -22,6 +44,7 @@ impl ApiDocBuilder for ManagerApiDocBuilder { ComponentsBuilder::new() .schema_from::() .schema_from::() + .schema_from::() .build() } } diff --git a/rust/agama-server/src/web/docs/misc.rs b/rust/agama-server/src/web/docs/misc.rs index f788dccfeb..2d79b66dab 100644 --- a/rust/agama-server/src/web/docs/misc.rs +++ b/rust/agama-server/src/web/docs/misc.rs @@ -1,3 +1,23 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; use super::ApiDocBuilder; diff --git a/rust/agama-server/src/web/docs/network.rs b/rust/agama-server/src/web/docs/network.rs index 23ad3de3cb..a2cd393587 100644 --- a/rust/agama-server/src/web/docs/network.rs +++ b/rust/agama-server/src/web/docs/network.rs @@ -1,5 +1,25 @@ -use serde_json::json; -use utoipa::openapi::{Components, ComponentsBuilder, ObjectBuilder, Paths, PathsBuilder}; +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +use agama_lib::openapi::schemas; +use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; use super::ApiDocBuilder; @@ -15,12 +35,15 @@ impl ApiDocBuilder for NetworkApiDocBuilder { .path_from::() .path_from::() .path_from::() + .path_from::() .path_from::() .path_from::() .path_from::() .path_from::() + .path_from::() .path_from::() - .path_from::() + .path_from::() + .path_from::() .build() } @@ -38,6 +61,7 @@ impl ApiDocBuilder for NetworkApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() @@ -47,6 +71,7 @@ impl ApiDocBuilder for NetworkApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() @@ -71,31 +96,9 @@ impl ApiDocBuilder for NetworkApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema( - "IpAddr", - ObjectBuilder::new() - .schema_type(utoipa::openapi::SchemaType::String) - .description(Some("An IP address (IPv4 or IPv6)".to_string())) - .example(Some(json!("192.168.1.100"))) - .build(), - ) - .schema( - "IpInet", - ObjectBuilder::new() - .schema_type(utoipa::openapi::SchemaType::String) - .description(Some( - "An IP address (IPv4 or IPv6) including the prefix".to_string(), - )) - .example(Some(json!("192.168.1.254/24"))) - .build(), - ) - .schema( - "macaddr.MacAddr6", - ObjectBuilder::new() - .schema_type(utoipa::openapi::SchemaType::String) - .description(Some("MAC address in EUI-48 format".to_string())) - .build(), - ) + .schema("IpAddr", schemas::ip_addr()) + .schema("IpInet", schemas::ip_inet()) + .schema("macaddr.MacAddr6", schemas::mac_addr6()) .build() } } diff --git a/rust/agama-server/src/web/docs/questions.rs b/rust/agama-server/src/web/docs/questions.rs index b69d30b585..bd302e85c8 100644 --- a/rust/agama-server/src/web/docs/questions.rs +++ b/rust/agama-server/src/web/docs/questions.rs @@ -1,3 +1,23 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; use super::ApiDocBuilder; diff --git a/rust/agama-server/src/web/docs/software.rs b/rust/agama-server/src/web/docs/software.rs index 2fe9da2790..2b52a31c2e 100644 --- a/rust/agama-server/src/web/docs/software.rs +++ b/rust/agama-server/src/web/docs/software.rs @@ -1,3 +1,23 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; use super::ApiDocBuilder; diff --git a/rust/agama-server/src/web/docs/storage.rs b/rust/agama-server/src/web/docs/storage.rs index 0d492c4057..441eafa945 100644 --- a/rust/agama-server/src/web/docs/storage.rs +++ b/rust/agama-server/src/web/docs/storage.rs @@ -1,3 +1,23 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + use utoipa::openapi::{Components, ComponentsBuilder, Paths, PathsBuilder}; use super::ApiDocBuilder; @@ -21,6 +41,13 @@ impl ApiDocBuilder for StorageApiDocBuilder { .path_from::() .path_from::() .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() .path_from::() .path_from::() .path_from::() @@ -29,6 +56,17 @@ impl ApiDocBuilder for StorageApiDocBuilder { .path_from::() .path_from::() .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() + .path_from::() .build() } @@ -39,15 +77,16 @@ impl ApiDocBuilder for StorageApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() .schema_from::() + .schema_from::() .schema_from::() .schema_from::() - .schema_from::() .schema_from::() .schema_from::() .schema_from::() @@ -66,6 +105,9 @@ impl ApiDocBuilder for StorageApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() + .schema_from::() + .schema_from::() .schema_from::() .schema_from::() .schema_from::() diff --git a/rust/agama-server/src/web/docs/users.rs b/rust/agama-server/src/web/docs/users.rs index 5826334f5c..7daae29609 100644 --- a/rust/agama-server/src/web/docs/users.rs +++ b/rust/agama-server/src/web/docs/users.rs @@ -1,3 +1,23 @@ +// Copyright (c) [2024] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + use utoipa::openapi::{ComponentsBuilder, Paths, PathsBuilder}; use super::ApiDocBuilder; diff --git a/rust/agama-server/src/web/http.rs b/rust/agama-server/src/web/http.rs index 3b019a7da5..8e845f1874 100644 --- a/rust/agama-server/src/web/http.rs +++ b/rust/agama-server/src/web/http.rs @@ -40,30 +40,39 @@ pub struct PingResponse { status: String, } -#[utoipa::path(get, path = "/ping", responses( - (status = 200, description = "The API is working", body = PingResponse) -))] +#[utoipa::path( + get, + path = "/ping", + context_path = "/api", + responses( + (status = 200, description = "The API is working", body = PingResponse) + ) +)] pub async fn ping() -> Json { Json(PingResponse { status: "success".to_string(), }) } -#[derive(Serialize)] +#[derive(Serialize, utoipa::ToSchema)] pub struct AuthResponse { /// Bearer token to use on subsequent calls token: String, } -#[derive(Deserialize)] +#[derive(Deserialize, utoipa::ToSchema)] pub struct LoginRequest { /// User password pub password: String, } -#[utoipa::path(post, path = "/api/auth", responses( - (status = 200, description = "The user has been successfully authenticated.", body = AuthResponse) -))] +#[utoipa::path(post, + path = "/auth", + context_path = "/api", + responses( + (status = 200, description = "The user has been successfully authenticated.", body = AuthResponse) + ) +)] pub async fn login( State(state): State, Json(login): Json, diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 9c13557a3a..3ff0c91ccd 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Mon Nov 11 09:52:59 UTC 2024 - Imobach Gonzalez Sosa + +- Add many missing elements to the OpenAPI spec + (gh#agama-project/agama#1700). +- Update to utoipa 5.2 to generate the OpenAPI spec. + ------------------------------------------------------------------- Thu Nov 7 14:20:48 UTC 2024 - Imobach Gonzalez Sosa diff --git a/rust/xtask/README.md b/rust/xtask/README.md index cdf17a48cc..b2e658a915 100644 --- a/rust/xtask/README.md +++ b/rust/xtask/README.md @@ -6,10 +6,11 @@ maintenance tasks using Rust code. ## Defined tasks -- `manpages`: generates manpages for the command-line interface. - `completions`: generates shell completion snippets for Bash, Fish and Zsh. +- `manpages`: generates manpages for the command-line interface. - `markdown`: generates a manual page for the command-line interface in Markdown format. Useful to be included in our website. +- `openapi`: generates the OpenAPI specification for the HTTP API. ## Running a task