diff --git a/products.d/agama-products.changes b/products.d/agama-products.changes index 2c70ceea4e..7802d73762 100644 --- a/products.d/agama-products.changes +++ b/products.d/agama-products.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Jan 28 13:01:02 UTC 2026 - Imobach Gonzalez Sosa + +- Fix the optional_patterns attribute in the product definitions + (jsc#PED-14307). + ------------------------------------------------------------------- Wed Jan 28 11:01:02 UTC 2026 - Imobach Gonzalez Sosa diff --git a/products.d/kalpa.yaml b/products.d/kalpa.yaml index e5312f5884..e4bdb0a385 100644 --- a/products.d/kalpa.yaml +++ b/products.d/kalpa.yaml @@ -86,7 +86,7 @@ software: - microos_hardware - microos_kde_desktop - microos_selinux - optional_patterns: null + optional_patterns: [] user_patterns: - container_runtime mandatory_packages: diff --git a/products.d/leap_160.yaml b/products.d/leap_160.yaml index 7d64ffd254..762cd373ea 100644 --- a/products.d/leap_160.yaml +++ b/products.d/leap_160.yaml @@ -63,7 +63,7 @@ software: archs: ppc mandatory_patterns: - enhanced_base # only pattern that is shared among all roles on Leap - optional_patterns: null # no optional pattern shared + optional_patterns: [] # no optional pattern shared user_patterns: - gnome - kde diff --git a/products.d/leap_micro_62.yaml b/products.d/leap_micro_62.yaml index 633915b48a..1d4a235005 100644 --- a/products.d/leap_micro_62.yaml +++ b/products.d/leap_micro_62.yaml @@ -29,7 +29,7 @@ software: - hardware - selinux - optional_patterns: null + optional_patterns: [] user_patterns: - cloud diff --git a/products.d/microos.yaml b/products.d/microos.yaml index 407b64b1a2..73ce02619f 100644 --- a/products.d/microos.yaml +++ b/products.d/microos.yaml @@ -138,7 +138,7 @@ software: - microos_defaults - microos_hardware - microos_selinux - optional_patterns: null + optional_patterns: [] user_patterns: - container_runtime - microos_ra_agent diff --git a/products.d/sles_sap_161.yaml b/products.d/sles_sap_161.yaml index 17b2f67072..3d17cd0d0a 100644 --- a/products.d/sles_sap_161.yaml +++ b/products.d/sles_sap_161.yaml @@ -88,7 +88,7 @@ software: - enhanced_base - bootloader - sles_sap_base_sap_server - optional_patterns: null # no optional pattern shared + optional_patterns: [] # no optional pattern shared user_patterns: # First all patterns from file sles_160.yaml - cockpit diff --git a/products.d/slowroll.yaml b/products.d/slowroll.yaml index 6ca52f72bc..393a09c1d0 100644 --- a/products.d/slowroll.yaml +++ b/products.d/slowroll.yaml @@ -88,7 +88,7 @@ software: mandatory_patterns: - enhanced_base - optional_patterns: null + optional_patterns: [] user_patterns: - basic-desktop - gnome diff --git a/products.d/tumbleweed.yaml b/products.d/tumbleweed.yaml index e7b8b7534b..66a79fb66f 100644 --- a/products.d/tumbleweed.yaml +++ b/products.d/tumbleweed.yaml @@ -121,7 +121,7 @@ software: archs: ppc mandatory_patterns: - enhanced_base # only pattern that is shared among all roles on TW - optional_patterns: null # no optional pattern shared + optional_patterns: [] # no optional pattern shared user_patterns: - basic_desktop - xfce diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index 03a14255df..25e648d9b3 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -40,7 +40,7 @@ use gettextrs::gettext; use merge::Merge; use network::NetworkSystemClient; use serde_json::Value; -use std::{process::Command, sync::Arc}; +use std::{collections::HashMap, process::Command, sync::Arc}; use tokio::sync::{broadcast, RwLock}; #[derive(Debug, thiserror::Error)] @@ -85,8 +85,12 @@ pub enum Error { Hardware(#[from] hardware::Error), #[error("Cannot dispatch this action in {current} stage (expected {expected}).")] UnexpectedStage { current: Stage, expected: Stage }, - #[error("Cannot start the installation. The config contains some issues.")] - InstallationBlocked, + #[error("Failed to perform the action because the system is busy: {scopes:?}.")] + Busy { scopes: Vec }, + #[error( + "It is not possible to install the system because there are some pending issues: {issues:?}." + )] + PendingIssues { issues: HashMap> }, #[error(transparent)] Users(#[from] users::service::Error), } @@ -515,6 +519,26 @@ impl Service { Ok(()) } + async fn check_issues(&self) -> Result<(), Error> { + let issues = self.issues.call(issue::message::Get).await?; + if !issues.is_empty() { + return Err(Error::PendingIssues { + issues: issues.clone(), + }); + } + Ok(()) + } + + async fn check_progress(&self) -> Result<(), Error> { + let progress = self.progress.call(progress::message::GetProgress).await?; + if !progress.is_empty() { + return Err(Error::Busy { + scopes: progress.iter().map(|p| p.scope).collect(), + }); + } + Ok(()) + } + /// Determines whether the software service is available. /// /// Consider the service as available if there is no pending progress. @@ -695,13 +719,6 @@ impl MessageHandler for Service { impl MessageHandler for Service { /// It runs the given action. async fn handle(&mut self, message: message::RunAction) -> Result<(), Error> { - let issues = self.issues.call(issue::message::Get).await?; - let progress = self.progress.call(progress::message::GetProgress).await?; - - if !issues.is_empty() || !progress.is_empty() { - return Err(Error::InstallationBlocked); - } - match message.action { Action::ConfigureL10n(config) => { self.check_stage(Stage::Configuring).await?; @@ -717,6 +734,8 @@ impl MessageHandler for Service { } Action::Install => { self.check_stage(Stage::Configuring).await?; + self.check_issues().await?; + self.check_progress().await?; let action = InstallAction { hostname: self.hostname.clone(), l10n: self.l10n.clone(), @@ -730,7 +749,7 @@ impl MessageHandler for Service { action.run(); } Action::Finish(method) => { - // TODO: check the stage + self.check_stage(Stage::Finished).await?; let action = FinishAction::new(method); action.run(); } @@ -803,6 +822,7 @@ impl InstallAction { /// Runs the installation process on a separate Tokio task. pub fn run(mut self) { tokio::spawn(async move { + tracing::info!("Installation started"); if let Err(error) = self.install().await { tracing::error!("Installation failed: {error}"); if let Err(error) = self @@ -815,6 +835,8 @@ impl InstallAction { Stage::Failed ); } + } else { + tracing::info!("Installation finished"); } }); } diff --git a/rust/agama-server/src/server/web.rs b/rust/agama-server/src/server/web.rs index 8365fdc9e0..d4cb24d205 100644 --- a/rust/agama-server/src/server/web.rs +++ b/rust/agama-server/src/server/web.rs @@ -22,7 +22,7 @@ use crate::server::config_schema; use agama_lib::{error::ServiceError, logs}; -use agama_manager::service::Error::InstallationBlocked; +use agama_manager::service::Error as ManagerError; use agama_manager::{self as manager, message}; use agama_software::Resolvable; use agama_utils::{ @@ -67,11 +67,17 @@ impl IntoResponse for Error { let body = json!({ "error": self.to_string() }); - let status = if matches!(self, Error::Manager(InstallationBlocked)) { - StatusCode::UNPROCESSABLE_ENTITY - } else { - StatusCode::BAD_REQUEST - }; + + let mut status = StatusCode::BAD_REQUEST; + + if let Error::Manager(error) = &self { + if matches!(error, ManagerError::PendingIssues { issues: _ }) + || matches!(error, ManagerError::Busy { scopes }) + { + status = StatusCode::UNPROCESSABLE_ENTITY; + } + } + (status, Json(body)).into_response() } } @@ -403,7 +409,7 @@ async fn get_license( path = "/action", context_path = "/api/v2", responses( - (status = 200, description = "Action successfully run."), + (status = 200, description = "Action successfully ran."), (status = 400, description = "Not possible to run the action.", body = Object), (status = 422, description = "Action blocked by backend state", body = Object) ), diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 2ea1c182f6..583746351c 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Wed Jan 28 12:43:05 UTC 2026 - Imobach Gonzalez Sosa + +- Fix the handling of HTTP actions (gh#agama-project/agama#3088). +- Add more details when a request fails because there are pending issues + or the installer is busy. +- Include in the logs markers for the start and the end of the + installation. + ------------------------------------------------------------------- Wed Jan 28 10:59:29 UTC 2026 - Imobach Gonzalez Sosa diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 45f0cb01ca..c80b3b44ca 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Wed Jan 28 12:43:13 UTC 2026 - Imobach Gonzalez Sosa + +- Force the user to select a mode when it is needed (related to + jsc#PED-14307). +- Do not loose the mode when registering a product (related to + jsc#PED-14307). +- Reset the configuration when selecting a new product. + ------------------------------------------------------------------- Wed Jan 28 11:00:28 UTC 2026 - Imobach Gonzalez Sosa diff --git a/web/src/components/product/ProductRegistrationPage.test.tsx b/web/src/components/product/ProductRegistrationPage.test.tsx index 6dbdad1d2a..2c8587f92e 100644 --- a/web/src/components/product/ProductRegistrationPage.test.tsx +++ b/web/src/components/product/ProductRegistrationPage.test.tsx @@ -80,7 +80,7 @@ jest.mock("~/hooks/model/proposal", () => ({ describe("ProductRegistrationPage", () => { beforeEach(() => { - mockConfig = { product: { id: "sle", registrationCode: "" } }; + mockConfig = { product: { id: "sle", mode: "standard", registrationCode: "" } }; mockIssues = []; mockProductConfig(mockConfig.product); // @ts-ignore @@ -144,6 +144,7 @@ describe("ProductRegistrationPage", () => { ...mockConfig, product: { id: "sle", + mode: "standard", registrationCode: "INTERNAL-USE-ONLY-1234-5678", registrationEmail: undefined, registrationUrl: undefined, @@ -172,6 +173,7 @@ describe("ProductRegistrationPage", () => { ...mockConfig, product: { id: "sle", + mode: "standard", registrationCode: "INTERNAL-USE-ONLY-1234-5678", registrationEmail: "example@company.test", registrationUrl: undefined, @@ -211,6 +213,7 @@ describe("ProductRegistrationPage", () => { ...mockConfig, product: { id: "sle", + mode: "standard", registrationUrl: "https://custom-server.test", registrationCode: undefined, registrationEmail: undefined, @@ -272,6 +275,7 @@ describe("ProductRegistrationPage", () => { ...mockConfig, product: { id: "sle", + mode: "standard", registrationUrl: "https://custom-server.test", registrationCode: "INTERNAL-USE-ONLY-1234-5678", registrationEmail: undefined, diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index ca782c61d4..e8149f9184 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -328,6 +328,7 @@ const RegistrationFormSection = () => { ...config, product: { id: product.id, + mode: product.mode, registrationCode: isKeyRequired ? key : undefined, registrationEmail: provideEmail ? email : undefined, registrationUrl: isUrlRequired ? url : undefined, diff --git a/web/src/components/product/ProductSelectionPage.test.tsx b/web/src/components/product/ProductSelectionPage.test.tsx index 603a4732ef..1f11f706cb 100644 --- a/web/src/components/product/ProductSelectionPage.test.tsx +++ b/web/src/components/product/ProductSelectionPage.test.tsx @@ -48,7 +48,7 @@ const microOs: Product = { modes: [], }; -const mockPatchConfigFn = jest.fn(); +const mockPutConfigFn = jest.fn(); const mockUseSystemFn: jest.Mock> = jest.fn(); const mockUseSystemSoftwareFn: jest.Mock> = jest.fn(); @@ -61,7 +61,7 @@ jest.mock("~/components/product/LicenseDialog", () => () =>
LicenseDialog M jest.mock("~/api", () => ({ ...jest.requireActual("~/api"), - patchConfig: (payload) => mockPatchConfigFn(payload), + putConfig: (payload) => mockPutConfigFn(payload), })); jest.mock("~/hooks/model/system", () => ({ @@ -183,7 +183,7 @@ describe("ProductSelectionPage", () => { const selectButton = screen.getByRole("button", { name: "Select" }); await user.click(productOption); await user.click(selectButton); - expect(mockPatchConfigFn).toHaveBeenCalledWith({ product: { id: tumbleweed.id } }); + expect(mockPutConfigFn).toHaveBeenCalledWith({ product: { id: tumbleweed.id } }); }); it("does not trigger the product selection if user selects a product but clicks o cancel button", async () => { @@ -194,7 +194,7 @@ describe("ProductSelectionPage", () => { expect(cancel).toHaveAttribute("href", ROOT.overview); await user.click(productOption); await user.click(cancel); - expect(mockPatchConfigFn).not.toHaveBeenCalled(); + expect(mockPutConfigFn).not.toHaveBeenCalled(); }); it.todo("make navigation test work"); diff --git a/web/src/components/product/ProductSelectionPage.tsx b/web/src/components/product/ProductSelectionPage.tsx index dcc02886ca..d191e8857a 100644 --- a/web/src/components/product/ProductSelectionPage.tsx +++ b/web/src/components/product/ProductSelectionPage.tsx @@ -56,7 +56,7 @@ import ProductLogo from "~/components/product/ProductLogo"; import LicenseDialog from "~/components/product/LicenseDialog"; import Text from "~/components/core/Text"; import agama from "~/agama"; -import { patchConfig } from "~/api"; +import { putConfig } from "~/api"; import { useProduct, useProductInfo } from "~/hooks/model/config/product"; import { useSystem } from "~/hooks/model/system"; import { useSystem as useSystemSoftware } from "~/hooks/model/system/software"; @@ -363,7 +363,10 @@ const ProductForm = ({ products, currentProduct, isSubmitted, onSubmit }: Produc const [eulaAccepted, setEulaAccepted] = useState(false); const mountEulaCheckbox = selectedProduct && !isEmpty(selectedProduct.license); const isSelectionDisabled = - !selectedProduct || isSubmitted || (mountEulaCheckbox && !eulaAccepted); + !selectedProduct || + isSubmitted || + (mountEulaCheckbox && !eulaAccepted) || + (!isEmpty(selectedProduct.modes) && !selectedMode); const onProductSelectionChange = (product) => { setEulaAccepted(false); @@ -532,7 +535,7 @@ const ProductSelectionContent = () => { setIsSubmmited(true); setSubmmitedSelection(selectedProduct); // FIXME: use Mode as expected - patchConfig({ product: { id: selectedProduct.id, mode: selectedMode } }); + putConfig({ product: { id: selectedProduct.id, mode: selectedMode } }); }; const introText = n_(