Skip to content

Commit

Permalink
fix(rust): add missing paths to the OpenAPI spec (#1737)
Browse files Browse the repository at this point in the history
The OpenAPI spec is not complete, as reported in #1700. This PR aims to
add the missing pieces to the spec. As part of the work, I updated the
utoipa dependency to version 5.2 which is much better when it comes to
detect omissions and problems.

## Tasks

- [x] Add missing `/api` prefix.
- [x] Add missing paths.
- [x] Add missing components.
- [x] Migrate to utoipa 5.
- [ ] Add the `issues` spec (postponed).
  • Loading branch information
imobachgs authored Nov 13, 2024
2 parents e162021 + edfe387 commit 9dc7bef
Show file tree
Hide file tree
Showing 30 changed files with 516 additions and 146 deletions.
33 changes: 4 additions & 29 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions rust/agama-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
4 changes: 3 additions & 1 deletion rust/agama-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
5 changes: 5 additions & 0 deletions rust/agama-lib/src/network/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -176,18 +177,22 @@ pub struct NetworkConnection {
pub method4: Option<String>,
/// Gateway IP address for the IPv4 connection
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(schema_with = schemas::ip_addr_ref)]
pub gateway4: Option<IpAddr>,
/// IPv6 method used for the network connection
#[serde(skip_serializing_if = "Option::is_none")]
pub method6: Option<String>,
/// Gateway IP address for the IPv6 connection
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(schema_with = schemas::ip_addr_ref)]
pub gateway6: Option<IpAddr>,
/// List of assigned IP addresses
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[schema(schema_with = schemas::ip_inet_array)]
pub addresses: Vec<IpInet>,
/// List of DNS server IP addresses
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[schema(schema_with = schemas::ip_addr_array)]
pub nameservers: Vec<IpAddr>,
/// List of search domains for DNS resolution
#[serde(skip_serializing_if = "Vec::is_empty", default)]
Expand Down
79 changes: 79 additions & 0 deletions rust/agama-lib/src/openapi.rs
Original file line number Diff line number Diff line change
@@ -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")
}
}
8 changes: 5 additions & 3 deletions rust/agama-lib/src/scripts/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,17 @@ 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 {
Pre,
Post,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(untagged)]
pub enum ScriptSource {
/// Script's body.
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion rust/agama-lib/src/storage/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Box<RawValue>>,
#[serde(default, rename = "legacyAutoyastStorage")]
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = String)]
pub storage_autoyast: Option<Box<RawValue>>,
}

Expand Down
2 changes: 1 addition & 1 deletion rust/agama-locale-data/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 2 additions & 2 deletions rust/agama-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"
Expand Down
11 changes: 8 additions & 3 deletions rust/agama-server/src/l10n/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LocaleEntry>)
))]
#[utoipa::path(
get,
path = "/locales",
context_path = "/api/l10n",
responses(
(status = 200, description = "List of known locales", body = Vec<LocaleEntry>)
)
)]
async fn locales(State(state): State<LocaleState<'_>>) -> Json<Vec<LocaleEntry>> {
let data = state.locale.read().await;
let locales = data.locales_db.entries().to_vec();
Expand Down
38 changes: 23 additions & 15 deletions rust/agama-server/src/manager/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -215,20 +215,23 @@ async fn installer_status(
fn logs_router() -> Router<ManagerState<'static>> {
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);
Expand All @@ -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))
Expand All @@ -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<LogsLists> {
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<logs::LogsLists> {
Json(logs::list())
}
Loading

0 comments on commit 9dc7bef

Please sign in to comment.