From 9f43987d53a2efdd046c471d4bf5ce98523fbee8 Mon Sep 17 00:00:00 2001 From: Nofar Date: Tue, 2 Sep 2025 16:35:51 +0300 Subject: [PATCH 1/4] Add VMaaS reposcan sync task for repository sync (#1078) Triggers VMaaS vulnerability scanning updates when Katello repositories are synchronized. (cherry picked from commit ee3700a9991b3021eb471e8e2c0bfc311b37efdd) --- lib/insights_cloud.rb | 4 + .../async/vmaas_reposcan_sync.rb | 71 +++++++++ .../async/vmaas_reposcan_sync_test.rb | 137 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 lib/insights_cloud/async/vmaas_reposcan_sync.rb create mode 100644 test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb diff --git a/lib/insights_cloud.rb b/lib/insights_cloud.rb index 3a710a0b1..cb52849f1 100644 --- a/lib/insights_cloud.rb +++ b/lib/insights_cloud.rb @@ -44,4 +44,8 @@ def self.enable_client_param_inventory def self.enable_cloud_remediations_param 'enable_cloud_remediations' end + + def self.vmaas_reposcan_sync_url + ForemanRhCloud.iop_smart_proxy.url + '/api/vmaas-reposcan/sync' + end end diff --git a/lib/insights_cloud/async/vmaas_reposcan_sync.rb b/lib/insights_cloud/async/vmaas_reposcan_sync.rb new file mode 100644 index 000000000..5a3c4452e --- /dev/null +++ b/lib/insights_cloud/async/vmaas_reposcan_sync.rb @@ -0,0 +1,71 @@ +require 'rest-client' + +module InsightsCloud + module Async + # Triggers VMaaS reposcan sync via IoP gateway when repositories are synced + class VmaasReposcanSync < ::Actions::EntryAction + include ::ForemanRhCloud::CertAuth + + # Subscribe to Katello repository sync hook action, if available + def self.subscribe + 'Actions::Katello::Repository::SyncHook'.constantize + rescue NameError + Rails.logger.debug('VMaaS reposcan sync: Repository::SyncHook action not found') + nil + end + + def plan(repo, *_args) + return unless ::ForemanRhCloud.with_iop_smart_proxy? + + repo_id = repo.is_a?(Hash) ? (repo[:id] || repo['id']) : nil + unless repo_id + logger.error("VMaaS reposcan sync: missing repository id in SyncHook plan parameters: #{repo.inspect}") + return + end + + plan_self + end + + def run + url = ::InsightsCloud.vmaas_reposcan_sync_url + + response = execute_cloud_request( + method: :put, + url: url, + headers: { 'Content-Type' => 'application/json' } + ) + + if response.code >= 200 && response.code < 300 + message = "VMaaS reposcan sync triggered successfully: #{response.code}" + logger.info(message) + else + message = "VMaaS reposcan sync failed with status: #{response.code}, body: #{response.body}" + logger.error(message) + end + output[:message] = message + + response + rescue RestClient::ExceptionWithResponse => e + message = "VMaaS reposcan sync failed: #{e.response&.code} - #{e.response&.body}" + logger.error(message) + output[:message] = message + raise + rescue StandardError => e + message = "Error triggering VMaaS reposcan sync: #{e.message}, response: #{e.respond_to?(:response) ? e.response : nil}" + logger.error(message) + output[:message] = message + raise + end + + def rescue_strategy_for_self + Dynflow::Action::Rescue::Skip + end + + private + + def logger + action_logger + end + end + end +end diff --git a/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb b/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb new file mode 100644 index 000000000..2b0c27279 --- /dev/null +++ b/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb @@ -0,0 +1,137 @@ +require 'test_plugin_helper' +require 'foreman_tasks/test_helpers' + +class VmaasReposcanSyncTest < ActiveSupport::TestCase + include ForemanTasks::TestHelpers::WithInThreadExecutor + + setup do + @repo_payload = { id: 123 } + @expected_url = 'https://example.com/api/v1/vmaas/reposcan/sync' + InsightsCloud.stubs(:vmaas_reposcan_sync_url).returns(@expected_url) + ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true) + end + + # Planning behavior + test 'plan plans_self when repo payload has id and IoP is available' do + InsightsCloud::Async::VmaasReposcanSync.any_instance.expects(:plan_self).once + + ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + end + + test 'plan does not plan_self when repo payload is missing id' do + payload_without_id = {} + + mock_logger = mock('logger') + mock_logger.expects(:error).with { |msg| msg =~ /missing repository id/i } + InsightsCloud::Async::VmaasReposcanSync.any_instance.stubs(:logger).returns(mock_logger) + InsightsCloud::Async::VmaasReposcanSync.any_instance.expects(:plan_self).never + + ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, payload_without_id) + end + + test 'plan does not plan_self when IoP smart proxy is not available' do + ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false) + + InsightsCloud::Async::VmaasReposcanSync.any_instance.expects(:plan_self).never + + ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + end + + test 'plan skips repo_id validation when IoP smart proxy is not available' do + ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false) + payload_without_id = {} + + # Logger should not be called since IoP check returns early + InsightsCloud::Async::VmaasReposcanSync.any_instance.expects(:logger).never + InsightsCloud::Async::VmaasReposcanSync.any_instance.expects(:plan_self).never + + ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, payload_without_id) + end + + test 'plan does not plan_self when repository is nil' do + mock_logger = mock('logger') + mock_logger.expects(:error).with { |msg| msg =~ /missing repository id/i } + InsightsCloud::Async::VmaasReposcanSync.any_instance.stubs(:logger).returns(mock_logger) + InsightsCloud::Async::VmaasReposcanSync.any_instance.expects(:plan_self).never + + ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, nil) + end + + # Run behavior + test 'run triggers VMaaS reposcan sync successfully' do + mock_response = mock('response') + mock_response.stubs(:code).returns(200) + + InsightsCloud::Async::VmaasReposcanSync.any_instance + .expects(:execute_cloud_request) + .with do |params| + params[:method] == :put && + params[:url] == @expected_url && + params[:headers].is_a?(Hash) && + params[:headers]['Content-Type'] == 'application/json' + end + .returns(mock_response) + + task = ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + + assert_equal 'VMaaS reposcan sync triggered successfully: 200', task.output[:message] + end + + test 'run sets error message in task output for failed response' do + mock_response = mock('response') + mock_response.stubs(:code).returns(500) + mock_response.stubs(:body).returns('Internal Server Error') + + InsightsCloud::Async::VmaasReposcanSync.any_instance + .stubs(:execute_cloud_request) + .returns(mock_response) + + task = ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + + assert_equal 'VMaaS reposcan sync failed with status: 500, body: Internal Server Error', task.output[:message] + end + + test 'run sets error message in task output for RestClient exception' do + error_response = mock('error_response') + error_response.stubs(:code).returns(500) + error_response.stubs(:body).returns('Server Error') + exception = RestClient::ExceptionWithResponse.new(error_response) + + InsightsCloud::Async::VmaasReposcanSync.any_instance + .stubs(:execute_cloud_request) + .raises(exception) + + error = assert_raises(ForemanTasks::TaskError) do + ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + end + + assert_equal 'VMaaS reposcan sync failed: 500 - Server Error', error.task.output[:message] + end + + test 'run sets error message in task output for StandardError exception' do + InsightsCloud::Async::VmaasReposcanSync.any_instance + .stubs(:execute_cloud_request) + .raises(StandardError.new('Network timeout')) + + error = assert_raises(ForemanTasks::TaskError) do + ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + end + + # The task is available via main_action + assert_match(/Error triggering VMaaS reposcan sync: Network timeout, response: /, + error.task.main_action.output[:message]) + end + + test 'run logs and re-raises when cloud request returns error response' do + error_response = mock('error_response', code: 500, body: 'error') + exception = RestClient::ExceptionWithResponse.new(error_response) + + InsightsCloud::Async::VmaasReposcanSync.any_instance + .stubs(:execute_cloud_request) + .raises(exception) + + assert_raises(ForemanTasks::TaskError) do + ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + end + end +end From 8f27551bc3547325b7e3fa229c7abf8b690138c7 Mon Sep 17 00:00:00 2001 From: Shimon Shtein Date: Mon, 15 Sep 2025 11:18:42 -0400 Subject: [PATCH 2/4] Add X-Org-Id header for IoP requests (cherry picked from commit d667c27b1592ca169fb50142ed16622dded486a8) --- lib/insights_cloud/async/vmaas_reposcan_sync.rb | 9 ++++++++- .../async/vmaas_reposcan_sync_test.rb | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/insights_cloud/async/vmaas_reposcan_sync.rb b/lib/insights_cloud/async/vmaas_reposcan_sync.rb index 5a3c4452e..3395151f7 100644 --- a/lib/insights_cloud/async/vmaas_reposcan_sync.rb +++ b/lib/insights_cloud/async/vmaas_reposcan_sync.rb @@ -23,13 +23,16 @@ def plan(repo, *_args) return end - plan_self + organization_id = Katello::Repository.find(repo_id).organization_id + + plan_self(organization_id: organization_id) end def run url = ::InsightsCloud.vmaas_reposcan_sync_url response = execute_cloud_request( + organization: organization, method: :put, url: url, headers: { 'Content-Type' => 'application/json' } @@ -61,6 +64,10 @@ def rescue_strategy_for_self Dynflow::Action::Rescue::Skip end + def organization + @organization ||= Organization.find(input[:organization_id]) + end + private def logger diff --git a/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb b/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb index 2b0c27279..9809180fd 100644 --- a/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb +++ b/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb @@ -5,7 +5,16 @@ class VmaasReposcanSyncTest < ActiveSupport::TestCase include ForemanTasks::TestHelpers::WithInThreadExecutor setup do - @repo_payload = { id: 123 } + @root = FactoryBot.build(:katello_root_repository, :fedora_17_x86_64_dev_root) + @root.save(validate: false) + @repo = FactoryBot.create( + :katello_repository, + :with_product, + distribution_family: 'Red Hat', + distribution_version: '7.5', + root: @root + ) + @repo_payload = { id: @repo.id } @expected_url = 'https://example.com/api/v1/vmaas/reposcan/sync' InsightsCloud.stubs(:vmaas_reposcan_sync_url).returns(@expected_url) ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true) @@ -68,7 +77,8 @@ class VmaasReposcanSyncTest < ActiveSupport::TestCase params[:method] == :put && params[:url] == @expected_url && params[:headers].is_a?(Hash) && - params[:headers]['Content-Type'] == 'application/json' + params[:headers]['Content-Type'] == 'application/json' && + params[:organization] == @repo.organization end .returns(mock_response) From 407a17b35ad5d6b264685e3cc20f2f19fe0bbf5a Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 20 Mar 2026 15:37:19 -0400 Subject: [PATCH 3/4] Fix VmaasReposcanSync errors preventing parent task success (#1180) * Fix VmaasReposcanSync errors preventing parent task success When VmaasReposcanSync receives an error from the IoP service (especially 429 during concurrent syncs), the action was re-raising exceptions which caused parent Katello repository sync tasks to fail. This fix removes the raise statements and allows the rescue_strategy_for_self Skip to work correctly, ensuring repository syncs succeed even when reposcan requests fail. Co-Authored-By: Claude Sonnet 4.5 (cherry picked from commit 38bc0e24a610c0b5f839cffcdb2785d7119091fc) --- .../async/vmaas_reposcan_sync.rb | 31 ++++-- .../async/vmaas_reposcan_sync_test.rb | 105 +++++++++++++----- 2 files changed, 103 insertions(+), 33 deletions(-) diff --git a/lib/insights_cloud/async/vmaas_reposcan_sync.rb b/lib/insights_cloud/async/vmaas_reposcan_sync.rb index 3395151f7..6dbc09131 100644 --- a/lib/insights_cloud/async/vmaas_reposcan_sync.rb +++ b/lib/insights_cloud/async/vmaas_reposcan_sync.rb @@ -6,6 +6,8 @@ module Async class VmaasReposcanSync < ::Actions::EntryAction include ::ForemanRhCloud::CertAuth + HTTP_TOO_MANY_REQUESTS = 429 + # Subscribe to Katello repository sync hook action, if available def self.subscribe 'Actions::Katello::Repository::SyncHook'.constantize @@ -49,15 +51,9 @@ def run response rescue RestClient::ExceptionWithResponse => e - message = "VMaaS reposcan sync failed: #{e.response&.code} - #{e.response&.body}" - logger.error(message) - output[:message] = message - raise + handle_rest_client_error(e) rescue StandardError => e - message = "Error triggering VMaaS reposcan sync: #{e.message}, response: #{e.respond_to?(:response) ? e.response : nil}" - logger.error(message) - output[:message] = message - raise + handle_standard_error(e) end def rescue_strategy_for_self @@ -70,6 +66,25 @@ def organization private + def handle_rest_client_error(exception) + if exception.response&.code == HTTP_TOO_MANY_REQUESTS + message = "VMaaS reposcan sync skipped: another sync already in progress (#{HTTP_TOO_MANY_REQUESTS})" + logger.warn(message) + else + message = "VMaaS reposcan sync failed: #{exception.response&.code} - #{exception.response&.body}" + logger.error(message) + end + output[:message] = message + # Do NOT raise - let rescue_strategy_for_self Skip handle this + end + + def handle_standard_error(exception) + message = "Error triggering VMaaS reposcan sync: #{exception.message}" + logger.error(message) + output[:message] = message + # Do NOT raise - let rescue_strategy_for_self Skip handle this + end + def logger action_logger end diff --git a/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb b/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb index 9809180fd..c39518a72 100644 --- a/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb +++ b/test/unit/lib/insights_cloud/async/vmaas_reposcan_sync_test.rb @@ -5,19 +5,17 @@ class VmaasReposcanSyncTest < ActiveSupport::TestCase include ForemanTasks::TestHelpers::WithInThreadExecutor setup do - @root = FactoryBot.build(:katello_root_repository, :fedora_17_x86_64_dev_root) - @root.save(validate: false) - @repo = FactoryBot.create( - :katello_repository, - :with_product, - distribution_family: 'Red Hat', - distribution_version: '7.5', - root: @root - ) + @organization = FactoryBot.create(:organization) + # Create a simple repository - we only need id and organization_id for the action + @repo = ::Katello::Repository.new(id: 1) + @repo.stubs(:organization_id).returns(@organization.id) + ::Katello::Repository.stubs(:find).with(1).returns(@repo) + @repo_payload = { id: @repo.id } @expected_url = 'https://example.com/api/v1/vmaas/reposcan/sync' InsightsCloud.stubs(:vmaas_reposcan_sync_url).returns(@expected_url) ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true) + Organization.stubs(:find).with(@organization.id).returns(@organization) end # Planning behavior @@ -78,7 +76,7 @@ class VmaasReposcanSyncTest < ActiveSupport::TestCase params[:url] == @expected_url && params[:headers].is_a?(Hash) && params[:headers]['Content-Type'] == 'application/json' && - params[:organization] == @repo.organization + params[:organization] == @organization end .returns(mock_response) @@ -111,37 +109,94 @@ class VmaasReposcanSyncTest < ActiveSupport::TestCase .stubs(:execute_cloud_request) .raises(exception) - error = assert_raises(ForemanTasks::TaskError) do - ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) - end + task = ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) - assert_equal 'VMaaS reposcan sync failed: 500 - Server Error', error.task.output[:message] + assert_equal 'VMaaS reposcan sync failed: 500 - Server Error', task.output[:message] end test 'run sets error message in task output for StandardError exception' do + mock_logger = mock('logger') + mock_logger.expects(:error).with('Error triggering VMaaS reposcan sync: Network timeout') + InsightsCloud::Async::VmaasReposcanSync.any_instance.stubs(:logger).returns(mock_logger) + InsightsCloud::Async::VmaasReposcanSync.any_instance .stubs(:execute_cloud_request) .raises(StandardError.new('Network timeout')) - error = assert_raises(ForemanTasks::TaskError) do - ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) - end + task = ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) - # The task is available via main_action - assert_match(/Error triggering VMaaS reposcan sync: Network timeout, response: /, - error.task.main_action.output[:message]) + assert_equal 'Error triggering VMaaS reposcan sync: Network timeout', task.output[:message] end - test 'run logs and re-raises when cloud request returns error response' do - error_response = mock('error_response', code: 500, body: 'error') + test 'run logs and handles error response without raising' do + error_response = mock('error_response') + error_response.stubs(:code).returns(500) + error_response.stubs(:body).returns('error') exception = RestClient::ExceptionWithResponse.new(error_response) InsightsCloud::Async::VmaasReposcanSync.any_instance .stubs(:execute_cloud_request) .raises(exception) - assert_raises(ForemanTasks::TaskError) do - ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) - end + task = ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + + assert_equal 'VMaaS reposcan sync failed: 500 - error', task.output[:message] + end + + test 'run handles 429 error with warning log level' do + error_response = mock('error_response') + error_response.stubs(:code).returns(429) + error_response.stubs(:body).returns('{"msg": "Another task already in progress"}') + exception = RestClient::ExceptionWithResponse.new(error_response) + + mock_logger = mock('logger') + mock_logger.expects(:warn).with('VMaaS reposcan sync skipped: another sync already in progress (429)') + InsightsCloud::Async::VmaasReposcanSync.any_instance.stubs(:logger).returns(mock_logger) + + InsightsCloud::Async::VmaasReposcanSync.any_instance + .stubs(:execute_cloud_request) + .raises(exception) + + task = ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + + assert_equal 'VMaaS reposcan sync skipped: another sync already in progress (429)', + task.output[:message] + end + + test 'run handles non-429 errors with error log level' do + error_response = mock('error_response') + error_response.stubs(:code).returns(500) + error_response.stubs(:body).returns('Internal Server Error') + exception = RestClient::ExceptionWithResponse.new(error_response) + + mock_logger = mock('logger') + mock_logger.expects(:error).with('VMaaS reposcan sync failed: 500 - Internal Server Error') + InsightsCloud::Async::VmaasReposcanSync.any_instance.stubs(:logger).returns(mock_logger) + + InsightsCloud::Async::VmaasReposcanSync.any_instance + .stubs(:execute_cloud_request) + .raises(exception) + + task = ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + + assert_equal 'VMaaS reposcan sync failed: 500 - Internal Server Error', + task.output[:message] + end + + test 'run handles RestClient::ExceptionWithResponse with nil response' do + exception = RestClient::ExceptionWithResponse.new(nil) + + mock_logger = mock('logger') + mock_logger.expects(:error).with('VMaaS reposcan sync failed: - ') + InsightsCloud::Async::VmaasReposcanSync.any_instance.stubs(:logger).returns(mock_logger) + + InsightsCloud::Async::VmaasReposcanSync.any_instance + .stubs(:execute_cloud_request) + .raises(exception) + + task = ForemanTasks.sync_task(InsightsCloud::Async::VmaasReposcanSync, @repo_payload) + + refute_nil task.output[:message] + assert_equal 'VMaaS reposcan sync failed: - ', task.output[:message] end end From ac5316b8ffcc34d08d5b31df12f46cac86af90c1 Mon Sep 17 00:00:00 2001 From: Odilon Sousa Date: Tue, 26 May 2026 11:41:59 -0300 Subject: [PATCH 4/4] Release 12.2.19 Co-Authored-By: Claude Sonnet 4.6 (1M context) --- lib/foreman_rh_cloud/version.rb | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/foreman_rh_cloud/version.rb b/lib/foreman_rh_cloud/version.rb index f1dd451b3..113ca094f 100644 --- a/lib/foreman_rh_cloud/version.rb +++ b/lib/foreman_rh_cloud/version.rb @@ -1,3 +1,3 @@ module ForemanRhCloud - VERSION = '12.2.18'.freeze + VERSION = '12.2.19'.freeze end diff --git a/package.json b/package.json index 0bdfbffec..2fe7d0ef5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foreman_rh_cloud", - "version": "12.2.18", + "version": "12.2.19", "description": "Inventory Upload =============", "main": "index.js", "scripts": {