diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index 138de166bf..e6103535e3 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -111,6 +111,8 @@ jobs: # send the code coverage for the Rust part to the coveralls.io - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2 + # ignore errors in this step + continue-on-error: true with: base-path: ./rust format: cobertura @@ -122,6 +124,8 @@ jobs: # only with the "parallel-finished: true" option) - name: Coveralls Finished uses: coverallsapp/github-action@v2 + # ignore errors in this step + continue-on-error: true with: parallel-finished: true carryforward: "service,web" diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 16b71740f6..45769fd109 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -135,7 +135,11 @@ impl<'a> ProductClient<'a> { .into_iter() .map(|(id, version, code)| AddonParams { id, - version, + version: if version.is_empty() { + None + } else { + Some(version) + }, registration_code: if code.is_empty() { None } else { Some(code) }, }) .collect(); @@ -158,7 +162,7 @@ impl<'a> ProductClient<'a> { .registration_proxy .register_addon( &addon.id, - &addon.version, + &addon.version.clone().unwrap_or_default(), &addon.registration_code.clone().unwrap_or_default(), ) .await?) diff --git a/rust/agama-lib/src/product/settings.rs b/rust/agama-lib/src/product/settings.rs index 28bf0f3cb8..c0eed23eac 100644 --- a/rust/agama-lib/src/product/settings.rs +++ b/rust/agama-lib/src/product/settings.rs @@ -27,7 +27,12 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct AddonSettings { pub id: String, - pub version: String, + /// Optional version of the addon, if not specified the version is found + /// from the available addons + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + /// Free extensions do not require a registration code + #[serde(skip_serializing_if = "Option::is_none")] pub registration_code: Option, } diff --git a/rust/agama-lib/src/software/model/registration.rs b/rust/agama-lib/src/software/model/registration.rs index 28e711f7b6..d4adf2537e 100644 --- a/rust/agama-lib/src/software/model/registration.rs +++ b/rust/agama-lib/src/software/model/registration.rs @@ -35,8 +35,8 @@ pub struct RegistrationParams { pub struct AddonParams { // Addon identifier pub id: String, - // Addon version, the same addon might be available in multiple versions - pub version: String, + // Addon version, if not specified the version is found from the available addons + pub version: Option, // Optional registration code, not required for free extensions pub registration_code: Option, } diff --git a/rust/package/agama.changes b/rust/package/agama.changes index d832727d4b..72f0888aaf 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Apr 1 12:44:57 UTC 2025 - Ladislav Slezák + +- Make the extension version attribute optional, search the version + automatically if it is missing (related to jsc#AGM-100) + ------------------------------------------------------------------- Fri Mar 28 21:45:05 UTC 2025 - Josef Reidinger diff --git a/service/lib/agama/autoyast/product_reader.rb b/service/lib/agama/autoyast/product_reader.rb index 90fcc0b637..24b0021251 100755 --- a/service/lib/agama/autoyast/product_reader.rb +++ b/service/lib/agama/autoyast/product_reader.rb @@ -83,9 +83,16 @@ def from_suse_register(section) # convert addons according to the new schema def convert_addons(addons) addons.map do |a| - addon = { "id" => a["name"], "version" => a["version"] } + addon = { "id" => a["name"] } + + version = a["version"].to_s + # omit the version if it was 11.x, 12.x or 15.x, the version is now optional + version = "" if version.match(/^1[125]\.\d$/) + addon["version"] = version unless version.empty? + code = a["reg_code"].to_s addon["registrationCode"] = code unless code.empty? + addon end end diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index 843200fa1b..ae1176b762 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -24,6 +24,7 @@ require "agama/dbus/base_object" require "agama/dbus/interfaces/issues" require "agama/dbus/clients/locale" +require "agama/errors" require "agama/registration" module Agama @@ -143,7 +144,8 @@ def registered_addons backend.registration.registered_addons.map do |addon| [ addon.name, - addon.version, + # return empty string if the version was not explicitly specified (was autodetected) + addon.required_version ? addon.version : "", addon.reg_code ] end @@ -199,7 +201,8 @@ def register(reg_code, email: nil) # have to explicitly call to #Probe after registering a product. # # @param name [String] name (id) of the addon, e.g. "sle-ha" - # @param version [String] version of the addon, e.g. "16.0" + # @param version [String] version of the addon, e.g. "16.0", if empty the version is found + # automatically in the list of available addons # @param reg_code [String] registration code, if the code is not required for the addon use # an empty string ("") # @@ -216,6 +219,8 @@ def register(reg_code, email: nil) # 8: incorrect credentials # 9: invalid certificate # 10: internal error (e.g., parsing json data) + # 11: addon not found + # 12: addon found in multiple versions def register_addon(name, version, reg_code) if !backend.product [1, "Product not selected yet"] @@ -335,6 +340,10 @@ def connect_result(first_error_code: 1, &block) connect_result_from_error(e, first_error_code + 5, "invalid certificate") rescue JSON::ParserError => e connect_result_from_error(e, first_error_code + 6) + rescue Errors::Registration::ExtensionNotFound => e + connect_result_from_error(e, first_error_code + 7) + rescue Errors::Registration::MultipleExtensionsFound => e + connect_result_from_error(e, first_error_code + 8) end # Generates a result from a given error. diff --git a/service/lib/agama/errors.rb b/service/lib/agama/errors.rb index db6a1915fa..8fb2d21959 100644 --- a/service/lib/agama/errors.rb +++ b/service/lib/agama/errors.rb @@ -24,5 +24,22 @@ module Agama module Errors # Invalid value given by the user class InvalidValue < StandardError; end + + # Registration specific errors + module Registration + # The requested extension was not found + class ExtensionNotFound < StandardError + def initialize(name) + super("#{name.inspect} is not available") + end + end + + # The requested extension exists in multiple versions + class MultipleExtensionsFound < StandardError + def initialize(name, versions) + super("#{name.inspect} is available in multiple versions: #{versions.join(", ")}") + end + end + end end end diff --git a/service/lib/agama/registered_addon.rb b/service/lib/agama/registered_addon.rb index f07fdb1e80..65622b1552 100644 --- a/service/lib/agama/registered_addon.rb +++ b/service/lib/agama/registered_addon.rb @@ -32,14 +32,20 @@ class RegisteredAddon # @return [String] attr_reader :version + # The addon version was explicitly specified by the user or it was autodetected. + # + # @return [Boolean] true if explicitly specified by user, false when autodetected + attr_reader :required_version + # Code used for registering the addon. # # @return [String] empty string if the registration code is not required attr_reader :reg_code - def initialize(name, version, reg_code = "") + def initialize(name, version, required_version, reg_code = "") @name = name @version = version + @required_version = required_version @reg_code = reg_code end end diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index 5afe358074..0b67a08c47 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -27,6 +27,7 @@ require "y2packager/resolvable" require "agama/cmdline_args" +require "agama/errors" require "agama/registered_addon" Yast.import "Arch" @@ -90,13 +91,8 @@ def register(code, email: "") # TODO: check if we can do it in memory for libzypp SUSE::Connect::YaST.create_credentials_file(@login, @password, GLOBAL_CREDENTIALS_PATH) - target_product = OpenStruct.new( - arch: Yast::Arch.rpm_arch, - identifier: product.id, - version: product.version || "1.0" - ) activate_params = {} - service = SUSE::Connect::YaST.activate_product(target_product, activate_params, email) + service = SUSE::Connect::YaST.activate_product(base_target_product, activate_params, email) process_service(service) @reg_code = code @@ -105,25 +101,33 @@ def register(code, email: "") end def register_addon(name, version, code) - if @registered_addons.any? { |a| a.name == name && a.version == version } - @logger.info "Addon #{name}-#{version} already registered, skipping registration" + register_version = if version.empty? + # version is not specified, find it automatically + find_addon_version(name) + else + # use the explicitly required version + version + end + + if @registered_addons.any? { |a| a.name == name && a.version == register_version } + @logger.info "Addon #{name}-#{register_version} already registered, skipping registration" return end - @logger.info "Registering addon #{name}-#{version}" + @logger.info "Registering addon #{name}-#{register_version}" # do not log the code, but at least log if it is empty @logger.info "Using empty registration code" if code.empty? target_product = OpenStruct.new( arch: Yast::Arch.rpm_arch, identifier: name, - version: version + version: register_version ) activate_params = { token: code } service = SUSE::Connect::YaST.activate_product(target_product, activate_params, @email) process_service(service) - @registered_addons << RegisteredAddon.new(name, version, code) + @registered_addons << RegisteredAddon.new(name, register_version, !version.empty?, code) # select the products to install @software.addon_products(find_addon_products) @@ -150,6 +154,7 @@ def deregister # reset @software.addon_products([]) @services = [] + @available_addons = nil reg_params = connect_params(token: reg_code, email: email) SUSE::Connect::YaST.deactivate_system(reg_params) @@ -314,5 +319,47 @@ def repository_data(repo) data["SrcId"] = repo data end + + # Get the available addons for the specified base product. + # + # @note The result is bound to the registration code used for the base product, the result + # might be different for different codes. E.g. the Alpha/Beta extensions might or might not + # be included in the list. + def available_addons + return @available_addons if @available_addons + + @available_addons = SUSE::Connect::YaST.show_product(base_target_product, + connect_params).extensions + @logger.info "Available addons: #{available_addons.inspect}" + @available_addons + end + + # Find the version for the specified addon, if none if multiple addons with the same name + # are found an exception is thrown. + # + # @return [String] the addon version, e.g. "16.0" + def find_addon_version(name) + raise Errors::Registration::ExtensionNotFound, name unless available_addons + + requested_addons = available_addons.select { |a| a.identifier == name } + case requested_addons.size + when 0 + raise Errors::Registration::ExtensionNotFound, name + when 1 + requested_addons.first.version + else + raise Errors::Registration::MultipleExtensionsFound.new(name, + requested_addons.map(&:version)) + end + end + + # Construct the base product data for sending to the server + def base_target_product + OpenStruct.new( + arch: Yast::Arch.rpm_arch, + identifier: product.id, + version: product.version || "1.0" + ) + end end end diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 56e86cfbd6..319641179a 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Apr 1 12:44:57 UTC 2025 - Ladislav Slezák + +- Make the extension version attribute optional, search the version + automatically if it is missing (related to jsc#AGM-100) + ------------------------------------------------------------------- Fri Mar 28 21:45:56 UTC 2025 - Josef Reidinger diff --git a/service/test/agama/registration_test.rb b/service/test/agama/registration_test.rb index b906fb19d9..49ecc9553c 100644 --- a/service/test/agama/registration_test.rb +++ b/service/test/agama/registration_test.rb @@ -239,6 +239,27 @@ let(:code) { "867136984314" } + let(:ha_extension) do + OpenStruct.new( + id: 2937, + identifier: "sle-ha", + version: "16.0", + arch: "x86_64", + isbase: false, + friendly_name: "SUSE Linux Enterprise High Availability Extension 16.0 x86_64 (BETA)", + ProductLine: "", + available: true, + free: false, + recommended: false, + description: "SUSE Linux High Availability Extension provides...", + former_identifier: "sle-ha", + product_type: "extension", + shortname: "SLEHA16", + name: "SUSE Linux Enterprise High Availability Extension", + release_stage: "beta" + ) + end + it "registers addon" do expect(SUSE::Connect::YaST).to receive(:activate_product).with( addon, { token: code }, anything @@ -255,6 +276,46 @@ subject.register_addon(addon.identifier, addon.version, code) subject.register_addon(addon.identifier, addon.version, code) end + + context "the requested addon version is not specified" do + it "finds the version automatically" do + expect(SUSE::Connect::YaST).to receive(:activate_product).with( + addon, { token: code }, anything + ) + + expect(SUSE::Connect::YaST).to receive(:show_product).and_return( + OpenStruct.new( + extensions: [ha_extension] + ) + ) + + subject.register_addon(addon.identifier, "", code) + end + + it "raises exception when the requested addon is not found" do + expect(SUSE::Connect::YaST).to receive(:show_product).and_return( + OpenStruct.new(extensions: []) + ) + + expect do + subject.register_addon(addon.identifier, "", code) + end.to raise_error(Agama::Errors::Registration::ExtensionNotFound) + end + + it "raises exception when multiple addon versions are found" do + ha1 = ha_extension + ha2 = ha1.dup + ha2.version = "42" + + expect(SUSE::Connect::YaST).to receive(:show_product).and_return( + OpenStruct.new(extensions: [ha1, ha2]) + ) + + expect do + subject.register_addon(addon.identifier, "", code) + end.to raise_error(Agama::Errors::Registration::MultipleExtensionsFound) + end + end end end