Skip to content

Commit

Permalink
feat(rust): implements the /patterns endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
imobachgs committed Dec 13, 2024
1 parent f73b699 commit 2ccbed4
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 16 deletions.
3 changes: 3 additions & 0 deletions rust/agama-server/src/software_ng/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ pub enum SoftwareServiceError {

#[error("Failed to load the repositories: {0}")]
LoadSourcesFailed(#[source] ZyppError),

#[error("Listing patterns failed: {0}")]
ListPatternsFailed(#[source] ZyppError),
}

/// Builds and starts the software service.
Expand Down
9 changes: 8 additions & 1 deletion rust/agama-server/src/software_ng/backend/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// 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::{product::Product, progress::ProgressSummary};
use agama_lib::{product::Product, progress::ProgressSummary, software::Pattern};
use tokio::sync::oneshot;

use crate::common::backend::service_status::ServiceStatusClient;
Expand Down Expand Up @@ -48,6 +48,13 @@ impl SoftwareServiceClient {
Ok(rx.await?)
}

/// Returns the list of known patterns.
pub async fn get_patterns(&self) -> Result<Vec<Pattern>, SoftwareServiceError> {
let (tx, rx) = oneshot::channel();
self.actions.send(SoftwareAction::GetPatterns(tx))?;
Ok(rx.await?)
}

pub async fn select_product(&self, product_id: &str) -> Result<(), SoftwareServiceError> {
self.actions
.send(SoftwareAction::SelectProduct(product_id.to_string()))?;
Expand Down
72 changes: 59 additions & 13 deletions rust/agama-server/src/software_ng/backend/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@

use std::{path::Path, sync::Arc};

use agama_lib::product::Product;
use agama_lib::{product::Product, software::Pattern};
use tokio::sync::{mpsc, oneshot, Mutex};

use crate::{
common::backend::service_status::{ServiceStatusClient, ServiceStatusManager},
products::ProductsRegistry,
products::{ProductSpec, ProductsRegistry},
web::EventsSender,
};

Expand All @@ -38,6 +38,7 @@ const GPG_KEYS: &str = "/usr/lib/rpm/gnupg/keys/gpg-*";
pub enum SoftwareAction {
Probe,
GetProducts(oneshot::Sender<Vec<Product>>),
GetPatterns(oneshot::Sender<Vec<Pattern>>),
SelectProduct(String),
}

Expand Down Expand Up @@ -108,6 +109,10 @@ impl SoftwareServiceServer {
self.get_products(tx).await?;
}

SoftwareAction::GetPatterns(tx) => {
self.get_patterns(tx).await?;
}

SoftwareAction::SelectProduct(product_id) => {
self.select_product(product_id).await?;
}
Expand Down Expand Up @@ -142,17 +147,7 @@ impl SoftwareServiceServer {
])
.await;

// FIXME: it holds the mutex too much. We could use a RwLock mutex.
let products = self.products.lock().await;

let Some(product_id) = &self.selected_product else {
return Err(SoftwareServiceError::NoSelectedProduct);
};

let Some(product) = products.find(product_id) else {
return Err(SoftwareServiceError::UnknownProduct(product_id.clone()));
};

let product = self.find_selected_product().await?;
let repositories = product.software.repositories();
for (idx, repo) in repositories.iter().enumerate() {
// TODO: we should add a repository ID in the configuration file.
Expand Down Expand Up @@ -198,6 +193,41 @@ impl SoftwareServiceServer {
Ok(())
}

async fn get_patterns(
&self,
tx: oneshot::Sender<Vec<Pattern>>,
) -> Result<(), SoftwareServiceError> {
let product = self.find_selected_product().await?;

let mandatory_patterns = product.software.mandatory_patterns.iter();
let optional_patterns = product.software.optional_patterns.unwrap_or(vec![]);
let optional_patterns = optional_patterns.iter();
let pattern_names: Vec<&str> = vec![mandatory_patterns, optional_patterns]
.into_iter()
.flatten()
.map(String::as_str)
.collect();

let patterns = zypp_agama::patterns_info(pattern_names)
.map_err(SoftwareServiceError::ListPatternsFailed)?;

let patterns = patterns
.into_iter()
.map(|info| Pattern {
name: info.name,
category: info.category,
description: info.description,
icon: info.icon,
summary: info.summary,
order: info.order,
})
.collect();

tx.send(patterns)
.map_err(|_| SoftwareServiceError::ResponseChannelClosed)?;
Ok(())
}

fn initialize_target_dir(&self) -> Result<(), SoftwareServiceError> {
let target_dir = Path::new(TARGET_DIR);
if target_dir.exists() {
Expand Down Expand Up @@ -229,4 +259,20 @@ impl SoftwareServiceServer {
}
}
}

// Returns the spec of the selected product.
//
// It causes the spec to be cloned, so we should find a better way to do this.
async fn find_selected_product(&self) -> Result<ProductSpec, SoftwareServiceError> {
let products = self.products.lock().await;
let Some(product_id) = &self.selected_product else {
return Err(SoftwareServiceError::NoSelectedProduct);
};

let Some(product) = products.find(product_id) else {
return Err(SoftwareServiceError::UnknownProduct(product_id.clone()));
};

Ok(product.clone())
}
}
24 changes: 22 additions & 2 deletions rust/agama-server/src/software_ng/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
// find current contact information at www.suse.com.

use agama_lib::{
error::ServiceError, product::Product, progress::ProgressSummary,
software::model::SoftwareConfig,
error::ServiceError,
product::Product,
progress::ProgressSummary,
software::{model::SoftwareConfig, Pattern},
};
use axum::{
extract::State,
Expand All @@ -40,6 +42,7 @@ struct SoftwareState {
pub async fn software_router(client: SoftwareServiceClient) -> Result<Router, ServiceError> {
let state = SoftwareState { client };
let router = Router::new()
.route("/patterns", get(get_patterns))
.route("/products", get(get_products))
// FIXME: it should be PATCH (using PUT just for backward compatibility).
.route("/config", put(set_config))
Expand Down Expand Up @@ -67,6 +70,23 @@ async fn get_products(State(state): State<SoftwareState>) -> Result<Json<Vec<Pro
Ok(Json(products))
}

/// Returns the list of available patterns.
///
/// * `state`: service state.
#[utoipa::path(
get,
path = "/patterns",
context_path = "/api/software_ng",
responses(
(status = 200, description = "List of product patterns", body = Vec<Pattern>),
(status = 400, description = "Cannot read the list of patterns")
)
)]
async fn get_patterns(State(state): State<SoftwareState>) -> Result<Json<Vec<Pattern>>, Error> {
let products = state.client.get_patterns().await?;
Ok(Json(products))
}

/// Sets the software configuration.
///
/// * `state`: service state.
Expand Down

0 comments on commit 2ccbed4

Please sign in to comment.