foreman_rh_cloud 14.4.0 → 14.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/rh_cloud/cloud_request_controller.rb +1 -0
  3. data/app/controllers/api/v2/rh_cloud/inventory_controller.rb +11 -5
  4. data/app/controllers/foreman_inventory_upload/accounts_controller.rb +0 -1
  5. data/app/controllers/foreman_inventory_upload/uploads_controller.rb +0 -11
  6. data/app/controllers/foreman_inventory_upload/uploads_settings_controller.rb +0 -8
  7. data/app/services/foreman_rh_cloud/cloud_presence.rb +23 -0
  8. data/config/routes.rb +1 -3
  9. data/lib/foreman_rh_cloud/engine.rb +1 -6
  10. data/lib/foreman_rh_cloud/plugin.rb +3 -2
  11. data/lib/foreman_rh_cloud/version.rb +1 -1
  12. data/lib/insights_cloud/async/cloud_connector_announce_task.rb +76 -19
  13. data/package.json +1 -1
  14. data/test/controllers/inventory_upload/api/inventory_controller_test.rb +10 -8
  15. data/test/jobs/cloud_connector_announce_task_test.rb +116 -106
  16. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/integrations.test.js +4 -12
  17. data/webpack/InsightsCloudSync/Components/InsightsTable/__tests__/InsightsTableActions.test.js +2 -2
  18. data/webpack/InsightsCloudSync/__tests__/InsightsCloudSyncActions.test.js +7 -5
  19. data/webpack/InsightsVulnerabilityActionsBar/__tests__/InsightsVulnerabilityActionsBar.test.js +1 -5
  20. data/webpack/test_setup.js +1 -0
  21. metadata +2 -8
  22. data/app/services/foreman_rh_cloud/cloud_connector.rb +0 -74
  23. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext.js +0 -6
  24. data/webpack/__mocks__/foremanReact/common/I18n.js +0 -6
  25. data/webpack/__mocks__/foremanReact/common/helpers.js +0 -14
  26. data/webpack/__mocks__/foremanReact/constants.js +0 -24
  27. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +0 -24
  28. data/webpack/__mocks__/foremanReact/redux/API/index.js +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '03458c064241e69e2f5384094f124fd0911a9bb2b08a0e299581c8d5c22ef5da'
4
- data.tar.gz: 4a14089d8e6410a5389473824d6206637370a34c552dcd178fe843b2eea3307c
3
+ metadata.gz: b3d41bcf3b5de41dab8c50751aecf9374fcb0bce44f1e0896011161b25ace53e
4
+ data.tar.gz: 8eb6a78729500fcb4ae06e8a35e452a0ed7a57e8dbc9f1d8ddf46b8c2a341016
5
5
  SHA512:
6
- metadata.gz: c5daadeadfe2543a242aea723e832b4fcc5f7ac31a598fc05f34792e1c58042ec993718e07b96bff3841b35f366438b8ed7e52166fe94ea118f635f9caadfa16
7
- data.tar.gz: c6008984ca0d88a5bcc51a1d6fd288e031b51238a0c561fcdb414cc967b7814750a529c5f5c0916866401dd922b71efd9fc20ad0b927fb2faec36ee21d5ae859
6
+ metadata.gz: d42639d1e4cb269096eb8cabe57a4337ee9c3e04e6339d2fdf8ac79ccf87188d60293820b5c6c61d79d03ecf40b5c042c2da8e81bb70251dda030cc24c74d1e2
7
+ data.tar.gz: c709b8044deff700aa3c541c6c21a9afdec88a449366785251057c64f1d85f73b9080b4391aca99d7564657509e3ee53c03de90f9f6d98dc9239d9de9a29f053
@@ -3,6 +3,7 @@ module Api::V2::RhCloud
3
3
  include ForemanRhCloud::IopSmartProxyAccess
4
4
  layout false
5
5
 
6
+ before_action :set_admin_user, only: [:update]
6
7
  before_action :require_non_iop_smart_proxy, only: [:update]
7
8
 
8
9
  KNOWN_DIRECTIVES = {
@@ -7,7 +7,7 @@ module Api
7
7
  include InventoryUpload::TaskActions
8
8
  include ForemanRhCloud::IopSmartProxyAccess
9
9
 
10
- before_action :require_non_iop_smart_proxy, only: [:enable_cloud_connector]
10
+ before_action :require_non_iop_smart_proxy, only: [:announce_to_sources]
11
11
 
12
12
  api :GET, "/organizations/:organization_id/rh_cloud/report", N_("Download latest report")
13
13
  param :organization_id, Integer, required: true, desc: N_("Set the current organization context for the request")
@@ -70,10 +70,16 @@ module Api
70
70
  render json: { message: error.message }, status: :bad_request
71
71
  end
72
72
 
73
- api :POST, "/rh_cloud/enable_connector", N_("Enable cloud connector")
74
- def enable_cloud_connector
75
- cloud_connector = ForemanRhCloud::CloudConnector.new
76
- render json: cloud_connector.install.to_json
73
+ api :POST, "/rh_cloud/announce_to_sources", N_("Schedule a task to register in Red Hat Sources for cloud connector")
74
+ param :instance_id, String, required: true, desc: N_("RHC instance ID to register with Red Hat Sources")
75
+ def announce_to_sources
76
+ Setting[:rhc_instance_id] = params[:instance_id]
77
+
78
+ task = ForemanTasks.async_task(InsightsCloud::Async::CloudConnectorAnnounceTask, true)
79
+
80
+ render json: {
81
+ task: task,
82
+ }, status: :accepted
77
83
  end
78
84
  end
79
85
  end
@@ -29,7 +29,6 @@ module ForemanInventoryUpload
29
29
 
30
30
  render json: {
31
31
  accounts: accounts,
32
- CloudConnectorStatus: ForemanInventoryUpload::UploadsSettingsController.cloud_connector_status,
33
32
  }, status: :ok
34
33
  end
35
34
 
@@ -1,22 +1,11 @@
1
1
  module ForemanInventoryUpload
2
2
  class UploadsController < ::ApplicationController
3
3
  include InventoryUpload::ReportActions
4
- include ForemanRhCloud::IopSmartProxyAccess
5
-
6
- before_action :require_non_iop_smart_proxy, only: [:enable_cloud_connector]
7
4
 
8
5
  def download_file
9
6
  filename, file = report_file(params[:organization_id])
10
7
 
11
8
  send_file file, disposition: 'attachment', filename: filename
12
9
  end
13
-
14
- def enable_cloud_connector
15
- # Set the autoupload to true, since it's required by the feature.
16
- Setting[:allow_auto_inventory_upload] = true
17
-
18
- cloud_connector = ForemanRhCloud::CloudConnector.new
19
- render json: cloud_connector.install.to_json
20
- end
21
10
  end
22
11
  end
@@ -9,7 +9,6 @@ module ForemanInventoryUpload
9
9
  ipsObfuscationEnabled: Setting[:obfuscate_inventory_ips],
10
10
  excludePackagesEnabled: Setting[:exclude_installed_packages],
11
11
  allowAutoInsightsMismatchDelete: Setting[:allow_auto_insights_mismatch_delete],
12
- CloudConnectorStatus: ForemanInventoryUpload::UploadsSettingsController.cloud_connector_status,
13
12
  lastSyncTask: last_successful_inventory_sync_task,
14
13
  }, status: :ok
15
14
  end
@@ -19,13 +18,6 @@ module ForemanInventoryUpload
19
18
  index
20
19
  end
21
20
 
22
- def self.cloud_connector_status
23
- cloud_connector = ForemanRhCloud::CloudConnector.new
24
- job = cloud_connector&.latest_job
25
- return nil unless job
26
- { id: job.id, task: ForemanTasks::Task.where(:id => job.task_id).first }
27
- end
28
-
29
21
  def last_successful_inventory_sync_task
30
22
  task = ForemanTasks::Task.where(label: 'InventorySync::Async::InventoryFullSync', result: 'success')
31
23
  .reorder('ended_at desc').first
@@ -74,6 +74,9 @@ module ForemanRhCloud
74
74
 
75
75
  def register_rhc_instance
76
76
  raise Foreman::Exception.new('rhc_instance_id is empty, cannot register RHC to the cloud') if Setting[:rhc_instance_id].empty?
77
+
78
+ return :already_registered if rhc_connection_exists?
79
+
77
80
  source_id = satellite_instance_source || create_satellite_instance_source
78
81
 
79
82
  create_response = JSON.parse(
@@ -95,6 +98,22 @@ module ForemanRhCloud
95
98
  @satellite_instance_source = create_response['id']
96
99
  end
97
100
 
101
+ def rhc_connection_exists?
102
+ response = JSON.parse(
103
+ execute_cloud_request(
104
+ method: :get,
105
+ url: rhc_connection_url,
106
+ headers: {
107
+ content_type: :json,
108
+ },
109
+ ssl_client_cert: OpenSSL::X509::Certificate.new(certs[:cert]),
110
+ ssl_client_key: OpenSSL::PKey.read(certs[:key])
111
+ )
112
+ )
113
+
114
+ response['data']&.any?
115
+ end
116
+
98
117
  private
99
118
 
100
119
  def sources_url(path)
@@ -113,6 +132,10 @@ module ForemanRhCloud
113
132
  sources_url('/sources')
114
133
  end
115
134
 
135
+ def rhc_connection_url
136
+ sources_url("/rhc_connections?filter[rhc_id]=#{Setting[:rhc_instance_id]}")
137
+ end
138
+
116
139
  def create_rhc_connections_url
117
140
  sources_url('/rhc_connections')
118
141
  end
data/config/routes.rb CHANGED
@@ -7,8 +7,6 @@ Rails.application.routes.draw do
7
7
  get 'settings', to: 'uploads_settings#index'
8
8
  post 'setting', to: 'uploads_settings#set_advanced_setting'
9
9
 
10
- post 'cloud_connector', to: 'uploads#enable_cloud_connector'
11
-
12
10
  resources :tasks, only: [:create, :show]
13
11
 
14
12
  get 'status', to: 'cloud_status#index'
@@ -72,7 +70,7 @@ Rails.application.routes.draw do
72
70
  end
73
71
 
74
72
  namespace 'rh_cloud' do
75
- post 'enable_connector', to: 'inventory#enable_cloud_connector'
73
+ post 'announce_to_sources', to: 'inventory#announce_to_sources'
76
74
  post 'cloud_request', to: 'cloud_request#update'
77
75
  get 'advisor_engine_config', to: 'advisor_engine_config#show'
78
76
 
@@ -61,6 +61,7 @@ module ForemanRhCloud
61
61
  ForemanRhCloud::Engine.register_scheduled_task(InventorySync::Async::InventoryScheduledSync, '0 0 * * *')
62
62
  ForemanRhCloud::Engine.register_scheduled_task(InsightsCloud::Async::InsightsScheduledSync, '0 0 * * *')
63
63
  ForemanRhCloud::Engine.register_scheduled_task(InsightsCloud::Async::InsightsClientStatusAging, '0 0 * * *')
64
+ ForemanRhCloud::Engine.register_scheduled_task(InsightsCloud::Async::CloudConnectorAnnounceTask, '0 0 * * *')
64
65
  end
65
66
  end
66
67
  rescue ActiveRecord::NoDatabaseError
@@ -80,12 +81,6 @@ module ForemanRhCloud
80
81
  host_action_button: false,
81
82
  provided_inputs: ['playbook_url', 'report_url', 'correlation_id', 'report_interval']
82
83
  )
83
- RemoteExecutionFeature.register(
84
- :ansible_configure_cloud_connector,
85
- N_('Configure Cloud Connector on given hosts'),
86
- :description => N_('Configure Cloud Connector on given hosts'),
87
- :proxy_selector_override => ::RemoteExecutionProxySelector::INTERNAL_PROXY
88
- )
89
84
  end
90
85
 
91
86
  # Ideally this code belongs to an initializer. The problem is that Katello controllers are not initialized completely until after the end of the to_prepare blocks
@@ -31,8 +31,7 @@ module ForemanRhCloud
31
31
  :generate_foreman_rh_cloud,
32
32
  'foreman_inventory_upload/reports': [:generate],
33
33
  'foreman_inventory_upload/tasks': [:create],
34
- 'api/v2/rh_cloud/inventory': [:get_hosts, :remove_hosts, :sync_inventory_status, :download_file, :generate_report, :enable_cloud_connector],
35
- 'foreman_inventory_upload/uploads': [:enable_cloud_connector],
34
+ 'api/v2/rh_cloud/inventory': [:get_hosts, :remove_hosts, :sync_inventory_status, :download_file, :generate_report, :announce_to_sources],
36
35
  'foreman_inventory_upload/uploads_settings': [:set_advanced_setting],
37
36
  'foreman_inventory_upload/missing_hosts': [:remove_hosts],
38
37
  'insights_cloud/settings': [:update],
@@ -123,6 +122,8 @@ module ForemanRhCloud
123
122
  role 'ForemanRhCloud Read Only', read_only_permissions, 'Role granting read-only permissions to view
124
123
  Insights Compliance, Vulnerability, Advisor, and host inventory'
125
124
 
125
+ role 'Cloud Connector', [:dispatch_cloud_requests], 'Role granting permission to dispatch cloud connector requests'
126
+
126
127
  add_permissions_to_default_roles Role::ORG_ADMIN => plugin_permissions,
127
128
  Role::MANAGER => plugin_permissions,
128
129
  Role::SYSTEM_ADMIN => plugin_permissions
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '14.4.0'.freeze
2
+ VERSION = '14.5.0'.freeze
3
3
  end
@@ -1,39 +1,96 @@
1
1
  module InsightsCloud
2
2
  module Async
3
3
  class CloudConnectorAnnounceTask < ::Actions::EntryAction
4
- def self.subscribe
5
- Actions::RemoteExecution::RunHostsJob
6
- end
4
+ include ::Actions::RecurringAction
5
+ include ::ForemanRhCloud::CertAuth
6
+ include ForemanInventoryUpload::Async::DelayedStart
7
7
 
8
- def self.connector_feature_id
9
- @connector_feature_id ||= RemoteExecutionFeature.feature!(ForemanRhCloud::CloudConnector::CLOUD_CONNECTOR_FEATURE).id
10
- end
8
+ def plan(immediate = false)
9
+ if ForemanRhCloud.with_iop_smart_proxy?
10
+ logger.debug('Sources announcement skipped: running in IoP mode')
11
+ return
12
+ end
11
13
 
12
- def plan(job_invocation)
13
- return unless connector_playbook_job?(job_invocation)
14
+ if Setting[:rhc_instance_id].blank?
15
+ logger.debug('Sources announcement skipped: rhc_instance_id is not set')
16
+ return
17
+ end
14
18
 
15
- plan_self
19
+ unless Setting[:allow_auto_inventory_upload]
20
+ logger.debug(
21
+ 'Cloud connector is configured (rhc_instance_id is set) but automatic inventory upload is disabled. ' \
22
+ 'Enable the "Automatic inventory upload" setting for full cloud connector functionality.'
23
+ )
24
+ end
25
+
26
+ if immediate
27
+ plan_self
28
+ else
29
+ after_delay do
30
+ plan_self
31
+ end
32
+ end
16
33
  end
17
34
 
18
- def finalize
35
+ def run
36
+ registered = []
37
+ confirmed_by_remediation = []
38
+ already_registered = []
39
+ skipped = []
40
+ failed = {}
41
+
19
42
  Organization.unscoped.each do |org|
43
+ unless cert_auth_available?(org)
44
+ skipped << org.name
45
+ next
46
+ end
47
+
48
+ if recent_cloud_remediation?(org)
49
+ confirmed_by_remediation << org.name
50
+ next
51
+ end
52
+
20
53
  presence = ForemanRhCloud::CloudPresence.new(org, logger)
21
- presence.announce_to_sources
54
+ result = presence.announce_to_sources
55
+ if result == :already_registered
56
+ already_registered << org.name
57
+ else
58
+ registered << org.name
59
+ end
22
60
  rescue StandardError => ex
23
- logger.warn(ex)
61
+ logger.warn("Failed to announce to Sources for organization #{org.name}: #{ex}")
62
+ logger.debug { ex.backtrace.join("\n") }
63
+ failed[org.name] = ex.message
24
64
  end
25
- end
26
65
 
27
- def rescue_strategy_for_self
28
- Dynflow::Action::Rescue::Skip
66
+ parts = []
67
+ parts << "Registered: #{registered.join(', ')}" if registered.any?
68
+ parts << "Already registered: #{already_registered.join(', ')}" if already_registered.any?
69
+ parts << "Already registered (recent cloud remediation): #{confirmed_by_remediation.join(', ')}" if confirmed_by_remediation.any?
70
+ parts << "Skipped (no manifest): #{skipped.join(', ')}" if skipped.any?
71
+ if failed.any?
72
+ failed_details = failed.map { |name, msg| "#{name}: #{msg}" }.join('; ')
73
+ parts << "Failed: #{failed_details}"
74
+ end
75
+ output[:status] = parts.join('. ')
76
+
77
+ error!("Sources announcement failed for: #{failed.keys.join(', ')}") if failed.any?
29
78
  end
30
79
 
31
- def connector_playbook_job?(job_invocation)
32
- job_invocation&.remote_execution_feature_id == connector_feature_id
80
+ def recent_cloud_remediation?(org)
81
+ feature = RemoteExecutionFeature.find_by(label: 'rh_cloud_connector_run_playbook')
82
+ return false unless feature
83
+
84
+ JobInvocation.where(remote_execution_feature_id: feature.id)
85
+ .joins(:task)
86
+ .joins(targeting: :hosts)
87
+ .where(hosts: { organization_id: org.id })
88
+ .where('foreman_tasks_tasks.started_at > ?', 24.hours.ago)
89
+ .exists?
33
90
  end
34
91
 
35
- def connector_feature_id
36
- self.class.connector_feature_id
92
+ def rescue_strategy_for_self
93
+ Dynflow::Action::Rescue::Skip
37
94
  end
38
95
 
39
96
  def logger
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "14.4.0",
3
+ "version": "14.5.0",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -35,20 +35,22 @@ module InventoryUpload::Api
35
35
  assert_equal test_task.id.to_s, actual_task['id']
36
36
  end
37
37
 
38
- test 'Starts cloud connector configuration job' do
39
- test_job = FactoryBot.create(:job_invocation)
38
+ test 'Triggers Sources announcement task' do
39
+ test_task = FactoryBot.create(:some_task)
40
40
 
41
- ForemanRhCloud::CloudConnector.any_instance
42
- .expects(:install)
43
- .returns(test_job)
41
+ ForemanTasks.expects(:async_task)
42
+ .with(InsightsCloud::Async::CloudConnectorAnnounceTask, true)
43
+ .returns(test_task)
44
44
 
45
- post :enable_cloud_connector
45
+ post :announce_to_sources, params: { instance_id: 'test-instance-id' }
46
46
 
47
47
  assert_response :success
48
48
 
49
- actual_job = @response.parsed_body
49
+ assert_equal 'test-instance-id', Setting[:rhc_instance_id]
50
50
 
51
- assert_equal test_job.id, actual_job['id']
51
+ actual_task = @response.parsed_body['task']
52
+ assert_not_nil actual_task
53
+ assert_equal test_task.id.to_s, actual_task['id']
52
54
  end
53
55
  end
54
56
  end
@@ -1,125 +1,135 @@
1
- require 'json'
2
1
  require 'test_plugin_helper'
3
2
  require 'foreman_tasks/test_helpers'
4
- require "#{ForemanTasks::Engine.root}/test/support/dummy_dynflow_action"
5
3
 
6
4
  class CloudConnectorAnnounceTaskTest < ActiveSupport::TestCase
7
5
  include Dynflow::Testing::Factories
8
6
 
9
7
  setup do
10
- RemoteExecutionFeature.register(
11
- :ansible_configure_cloud_connector,
12
- N_('Configure Cloud Connector on given hosts'),
13
- :description => N_('Configure Cloud Connector on given hosts'),
14
- :proxy_selector_override => ::RemoteExecutionProxySelector::INTERNAL_PROXY
15
- )
8
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
9
+ .stubs(:recent_cloud_remediation?).returns(false)
10
+ end
11
+
12
+ teardown do
13
+ ForemanRhCloud.unstub(:with_iop_smart_proxy?)
14
+ end
15
+
16
+ test 'announces to sources for all organizations when rhc_instance_id is set' do
17
+ Setting[:rhc_instance_id] = 'test-rhc-id'
18
+
19
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
20
+ .stubs(:cert_auth_available?).returns(true)
16
21
 
17
- @job_invocation = generate_job_invocation(:ansible_configure_cloud_connector)
22
+ ForemanRhCloud::CloudPresence.any_instance
23
+ .expects(:announce_to_sources)
24
+ .times(Organization.unscoped.count)
18
25
 
19
- # reset connector feature ID cache
20
- InsightsCloud::Async::CloudConnectorAnnounceTask.instance_variable_set(:@connector_feature_id, nil)
26
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
27
+ action = run_action(action)
28
+
29
+ status = action.output[:status].to_s
30
+ assert_match(/Registered:|Already registered:/, status)
31
+ refute_match(/Skipped/, status)
32
+ refute_match(/Failed/, status)
21
33
  end
22
34
 
23
- test 'It executes cloud presence announcer' do
24
- ForemanRhCloud::CloudPresence.any_instance.expects(:announce_to_sources).times(Organization.unscoped.count)
35
+ test 'skips when rhc_instance_id is not set' do
36
+ Setting[:rhc_instance_id] = nil
37
+
38
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
25
39
 
26
- action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask, @job_invocation)
27
- finalize_action(action)
40
+ assert_empty action.execution_plan.planned_run_steps
28
41
  end
29
42
 
30
- private
31
-
32
- def generate_job_invocation(feature_name)
33
- job_template = FactoryBot.build(
34
- :job_template,
35
- :template => 'BLEH'
36
- )
37
- feature = RemoteExecutionFeature.feature!(feature_name).id
38
-
39
- job_invocation = FactoryBot.create(
40
- :job_invocation,
41
- remote_execution_feature_id: feature,
42
- task_id: FactoryBot.create(:dynflow_task).id
43
- )
44
-
45
- job_template.template_inputs << playbook_url_input = FactoryBot.build(:template_input,
46
- :name => 'playbook_url',
47
- :input_type => 'user',
48
- :required => true)
49
- job_template.template_inputs << report_url_input = FactoryBot.build(:template_input,
50
- :name => 'report_url',
51
- :input_type => 'user',
52
- :required => true)
53
- job_template.template_inputs << correlation_id_input = FactoryBot.build(:template_input,
54
- :name => 'correlation_id',
55
- :input_type => 'user',
56
- :required => true)
57
- job_template.template_inputs << report_interval_input = FactoryBot.build(:template_input,
58
- :name => 'report_interval',
59
- :input_type => 'user',
60
- :required => true)
61
-
62
- template_invocation = FactoryBot.build(:template_invocation,
63
- :template => job_template,
64
- :job_invocation => job_invocation)
65
-
66
- template_invocation.input_values << FactoryBot.create(
67
- :template_invocation_input_value,
68
- :template_invocation => template_invocation,
69
- :template_input => playbook_url_input,
70
- :value => 'http://example.com/TEST_PLAYBOOK'
71
- )
72
- template_invocation.input_values << FactoryBot.create(
73
- :template_invocation_input_value,
74
- :template_invocation => template_invocation,
75
- :template_input => report_url_input,
76
- :value => 'http://example.com/TEST_REPORT'
77
- )
78
- template_invocation.input_values << FactoryBot.create(
79
- :template_invocation_input_value,
80
- :template_invocation => template_invocation,
81
- :template_input => correlation_id_input,
82
- :value => 'TEST_CORRELATION'
83
- )
84
- template_invocation.input_values << FactoryBot.create(
85
- :template_invocation_input_value,
86
- :template_invocation => template_invocation,
87
- :template_input => report_interval_input,
88
- :value => '1'
89
- )
90
-
91
- @host1 = FactoryBot.create(:host, :with_insights_hits, name: 'host1')
92
- @host1.insights.uuid = 'TEST_UUID1'
93
- @host1.insights.save!
94
- @host2 = FactoryBot.create(:host, :with_insights_hits, name: 'host2')
95
- @host2.insights.uuid = 'TEST_UUID2'
96
- @host2.insights.save!
97
-
98
- targeting = FactoryBot.create(:targeting, hosts: [@host1, @host2])
99
- job_invocation.targeting = targeting
100
- job_invocation.save!
101
-
102
- job_invocation.template_invocations << FactoryBot.create(
103
- :template_invocation,
104
- run_host_job_task: FactoryBot.create(:dynflow_task),
105
- host_id: @host1.id
106
- )
107
- job_invocation.template_invocations << FactoryBot.create(
108
- :template_invocation,
109
- run_host_job_task: FactoryBot.create(:dynflow_task),
110
- host_id: @host2.id
111
- )
112
-
113
- fake_output = (1..5).map do |i|
114
- { 'timestamp' => (Time.now - (5 - i)).to_f, 'output' => "#{i}\n" }
115
- end
116
- Support::DummyDynflowAction.any_instance.stubs(:live_output).returns(fake_output)
117
- Support::DummyDynflowAction.any_instance.stubs(:exit_status).returns(0)
43
+ test 'skips when rhc_instance_id is empty string' do
44
+ Setting[:rhc_instance_id] = ''
45
+
46
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
47
+
48
+ assert_empty action.execution_plan.planned_run_steps
49
+ end
50
+
51
+ test 'skips when in IoP mode' do
52
+ Setting[:rhc_instance_id] = 'test-rhc-id'
53
+ ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
54
+
55
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
56
+
57
+ assert_empty action.execution_plan.planned_run_steps
58
+ end
59
+
60
+ test 'skips organizations without a manifest' do
61
+ Setting[:rhc_instance_id] = 'test-rhc-id'
62
+
63
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
64
+ .stubs(:cert_auth_available?).returns(false)
65
+
66
+ ForemanRhCloud::CloudPresence.any_instance
67
+ .expects(:announce_to_sources)
68
+ .never
118
69
 
119
- job_invocation
70
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
71
+ action = run_action(action)
72
+
73
+ status = action.output[:status].to_s
74
+ assert_match(/Skipped \(no manifest\):/, status)
75
+ refute_match(/Registered/, status)
76
+ refute_match(/Failed/, status)
77
+ end
78
+
79
+ test 'still runs when allow_auto_inventory_upload is disabled' do
80
+ Setting[:rhc_instance_id] = 'test-rhc-id'
81
+ Setting[:allow_auto_inventory_upload] = false
82
+
83
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
84
+ .stubs(:cert_auth_available?).returns(true)
85
+
86
+ ForemanRhCloud::CloudPresence.any_instance
87
+ .expects(:announce_to_sources)
88
+ .times(Organization.unscoped.count)
89
+
90
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
91
+ action = run_action(action)
92
+
93
+ assert_match(/Registered:|Already registered:/, action.output[:status].to_s)
94
+ end
95
+
96
+ test 'skips orgs with recent cloud remediation jobs' do
97
+ Setting[:rhc_instance_id] = 'test-rhc-id'
98
+
99
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
100
+ .stubs(:cert_auth_available?).returns(true)
101
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
102
+ .stubs(:recent_cloud_remediation?).returns(true)
103
+
104
+ ForemanRhCloud::CloudPresence.any_instance
105
+ .expects(:announce_to_sources)
106
+ .never
107
+
108
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
109
+ action = run_action(action)
110
+
111
+ status = action.output[:status].to_s
112
+ assert_match(/Already registered \(recent cloud remediation\):/, status)
113
+ refute_match(/\bRegistered:/, status)
120
114
  end
121
115
 
122
- def read_jsonl(jsonl)
123
- jsonl.lines.map { |l| JSON.parse(l) }
116
+ test 'continues processing other orgs when one fails and task errors' do
117
+ Setting[:rhc_instance_id] = 'test-rhc-id'
118
+
119
+ InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance
120
+ .stubs(:cert_auth_available?).returns(true)
121
+
122
+ call_count = 0
123
+ ForemanRhCloud::CloudPresence.any_instance.stubs(:announce_to_sources).with do
124
+ call_count += 1
125
+ raise(StandardError.new('API error')) if call_count == 1
126
+ true
127
+ end
128
+
129
+ action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask)
130
+
131
+ error = assert_raises(StandardError) { run_action(action) }
132
+ assert_match(/Sources announcement failed for:/, error.message)
133
+ assert_equal Organization.unscoped.count, call_count
124
134
  end
125
135
  end
@@ -3,24 +3,16 @@ import { render, screen, fireEvent } from '@testing-library/react';
3
3
  import { Provider } from 'react-redux';
4
4
  import configureMockStore from 'redux-mock-store';
5
5
  import thunk from 'redux-thunk';
6
- import * as API from 'foremanReact/redux/API';
7
6
  import ConnectedSyncButton from '../index';
8
- import { successResponse } from './SyncButtonFixtures';
9
- import { INVENTORY_SYNC } from '../SyncButtonConstants';
10
-
11
- jest.spyOn(API, 'post');
12
7
 
13
8
  const mockStore = configureMockStore([thunk]);
14
9
 
10
+ afterEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
15
14
  describe('SyncButton integration test', () => {
16
15
  it('dispatches sync action when button is clicked', () => {
17
- API.post.mockImplementation(({ handleSuccess, key, ...action }) => {
18
- if (key === INVENTORY_SYNC && handleSuccess) {
19
- handleSuccess(successResponse);
20
- }
21
- return { type: 'API_POST', ...action };
22
- });
23
-
24
16
  const store = mockStore({
25
17
  API: {},
26
18
  });
@@ -117,10 +117,10 @@ describe('InsightsTable actions', () => {
117
117
  expect(push).toHaveBeenCalled();
118
118
 
119
119
  const apiAction = dispatch.mock.calls.find(
120
- call => call[0] && call[0].key === INSIGHTS_HITS_API_KEY
120
+ call => call[0] && call[0].payload?.key === INSIGHTS_HITS_API_KEY
121
121
  );
122
122
  expect(apiAction).toBeTruthy();
123
- const getArg = apiAction[0];
123
+ const getArg = apiAction[0].payload;
124
124
  expect(getArg.url).toBe(INSIGHTS_HITS_PATH);
125
125
  expect(getArg.params.page).toBe(2);
126
126
  expect(getArg.params.per_page).toBe(7);
@@ -1,3 +1,4 @@
1
+ import { API_OPERATIONS } from 'foremanReact/redux/API';
1
2
  import { syncInsights } from '../InsightsCloudSyncActions';
2
3
  import { INSIGHTS_CLOUD_SYNC } from '../InsightsCloudSyncConstants';
3
4
 
@@ -17,17 +18,18 @@ describe('InsightsCloudSync actions', () => {
17
18
 
18
19
  expect(dispatch).toHaveBeenCalledTimes(1);
19
20
  const dispatched = dispatch.mock.calls[0][0];
20
- expect(dispatched.key).toBe(INSIGHTS_CLOUD_SYNC);
21
- expect(dispatched.url).toBe('/insights_cloud/tasks');
22
- expect(typeof dispatched.handleSuccess).toBe('function');
23
- expect(typeof dispatched.errorToast).toBe('function');
21
+ expect(dispatched.type).toBe(API_OPERATIONS.POST);
22
+ expect(dispatched.payload.key).toBe(INSIGHTS_CLOUD_SYNC);
23
+ expect(dispatched.payload.url).toBe('/insights_cloud/tasks');
24
+ expect(typeof dispatched.payload.handleSuccess).toBe('function');
25
+ expect(typeof dispatched.payload.errorToast).toBe('function');
24
26
  });
25
27
 
26
28
  it('errorToast returns failure message with error details', () => {
27
29
  syncInsights(jest.fn(), '')(dispatch);
28
30
  const dispatched = dispatch.mock.calls[0][0];
29
31
 
30
- const result = dispatched.errorToast('some error');
32
+ const result = dispatched.payload.errorToast('some error');
31
33
  expect(result).toContain('some error');
32
34
  });
33
35
  });
@@ -1,13 +1,11 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
- import * as ForemanContext from 'foremanReact/Root/Context/ForemanContext';
5
4
  import { ForemanHostsIndexActionsBarContext } from 'foremanReact/components/HostsIndex';
6
5
  import * as ConfigHooks from '../../common/Hooks/ConfigHooks';
7
6
  import InsightsVulnerabilityActionsBar from '../index';
8
7
 
9
8
  jest.mock('../../common/Hooks/ConfigHooks');
10
- jest.mock('foremanReact/Root/Context/ForemanContext');
11
9
  jest.mock('../InsightsVulnerabilityActionsBarActions');
12
10
 
13
11
  const mockContextValue = {
@@ -16,11 +14,9 @@ const mockContextValue = {
16
14
  setMenuOpen: jest.fn(),
17
15
  };
18
16
 
19
- const renderComponent = (contextOverrides = {}, orgId = 1) => {
17
+ const renderComponent = (contextOverrides = {}) => {
20
18
  const contextValue = { ...mockContextValue, ...contextOverrides };
21
19
 
22
- ForemanContext.useForemanOrganization.mockReturnValue({ id: orgId });
23
-
24
20
  return render(
25
21
  <ForemanHostsIndexActionsBarContext.Provider value={contextValue}>
26
22
  <InsightsVulnerabilityActionsBar />
@@ -0,0 +1 @@
1
+ import 'foremanJSTestSetup';
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_rh_cloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 14.4.0
4
+ version: 14.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Red Hat Cloud team
@@ -107,7 +107,6 @@ files:
107
107
  - app/overrides/layouts/base/styles.html.erb.deface
108
108
  - app/services/foreman_rh_cloud/branch_info.rb
109
109
  - app/services/foreman_rh_cloud/cert_auth.rb
110
- - app/services/foreman_rh_cloud/cloud_connector.rb
111
110
  - app/services/foreman_rh_cloud/cloud_ping_service.rb
112
111
  - app/services/foreman_rh_cloud/cloud_presence.rb
113
112
  - app/services/foreman_rh_cloud/cloud_request.rb
@@ -478,20 +477,14 @@ files:
478
477
  - webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js
479
478
  - webpack/IopPathwayDetails/IopPathwayDetails.js
480
479
  - webpack/IopRecommendationDetails/IopRecommendationDetails.js
481
- - webpack/__mocks__/foremanReact/Root/Context/ForemanContext.js
482
- - webpack/__mocks__/foremanReact/common/I18n.js
483
480
  - webpack/__mocks__/foremanReact/common/MountingService.js
484
- - webpack/__mocks__/foremanReact/common/helpers.js
485
481
  - webpack/__mocks__/foremanReact/components/ConfirmModal/index.js
486
482
  - webpack/__mocks__/foremanReact/components/Head.js
487
483
  - webpack/__mocks__/foremanReact/components/HostsIndex/index.js
488
484
  - webpack/__mocks__/foremanReact/components/Layout/LayoutConstants.js
489
485
  - webpack/__mocks__/foremanReact/components/ToastsList/index.js
490
486
  - webpack/__mocks__/foremanReact/components/common/dates/RelativeDateTime.js
491
- - webpack/__mocks__/foremanReact/constants.js
492
487
  - webpack/__mocks__/foremanReact/redux.js
493
- - webpack/__mocks__/foremanReact/redux/API/APISelectors.js
494
- - webpack/__mocks__/foremanReact/redux/API/index.js
495
488
  - webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware.js
496
489
  - webpack/__mocks__/foremanReact/routes/RouterSelector.js
497
490
  - webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js
@@ -514,6 +507,7 @@ files:
514
507
  - webpack/common/table/helpers.js
515
508
  - webpack/global_index.js
516
509
  - webpack/index.js
510
+ - webpack/test_setup.js
517
511
  homepage: https://github.com/theforeman/foreman_rh_cloud
518
512
  licenses:
519
513
  - GPL-3.0
@@ -1,74 +0,0 @@
1
- require 'securerandom'
2
-
3
- module ForemanRhCloud
4
- class CloudConnector
5
- CLOUD_CONNECTOR_USER = 'cloud_connector_user'.freeze
6
- CLOUD_CONNECTOR_TOKEN_NAME = 'Cloud connector access token'.freeze
7
- CLOUD_CONNECTOR_FEATURE = 'ansible_configure_cloud_connector'.freeze
8
-
9
- def install
10
- user = service_user
11
- token_value = personal_access_token(user)
12
- target_host = foreman_host
13
- composer = nil
14
-
15
- input = {
16
- :satellite_cloud_connector_user => service_user.login,
17
- :satellite_cloud_connector_password => token_value,
18
- }
19
-
20
- if (http_proxy = ForemanRhCloud.proxy_setting)
21
- input[:satellite_cloud_connector_http_proxy] = http_proxy
22
- end
23
-
24
- Taxonomy.as_taxonomy(target_host.organization, target_host.location) do
25
- composer = ::JobInvocationComposer.for_feature(
26
- CLOUD_CONNECTOR_FEATURE,
27
- [target_host.id],
28
- input
29
- )
30
- composer.trigger!
31
- end
32
-
33
- composer.job_invocation
34
- end
35
-
36
- def latest_job
37
- feature_id = RemoteExecutionFeature.find_by_label(CLOUD_CONNECTOR_FEATURE)&.id
38
- return nil unless feature_id
39
- JobInvocation.where(:remote_execution_feature_id => feature_id).includes(:task).reorder('foreman_tasks_tasks.started_at DESC').first
40
- end
41
-
42
- def personal_access_token(user)
43
- access_token = PersonalAccessToken.find_by_name(CLOUD_CONNECTOR_TOKEN_NAME)
44
-
45
- access_token&.destroy! # destroy the old token if exists
46
-
47
- personal_access_token = PersonalAccessToken.new(:name => CLOUD_CONNECTOR_TOKEN_NAME, :user => user)
48
- token_value = personal_access_token.generate_token
49
- personal_access_token.save!
50
-
51
- token_value
52
- end
53
-
54
- def service_user
55
- user = User.find_by_login(CLOUD_CONNECTOR_USER)
56
-
57
- if user.nil?
58
- user = User.create!(
59
- :login => CLOUD_CONNECTOR_USER,
60
- :password => SecureRandom.base64(255),
61
- :description => "This is a service user used by cloud connector to talk to the Satellite API",
62
- :auth_source => AuthSourceInternal.first,
63
- :admin => true
64
- )
65
- end
66
-
67
- user
68
- end
69
-
70
- def foreman_host
71
- ForemanRhCloud.foreman_host
72
- end
73
- end
74
- end
@@ -1,6 +0,0 @@
1
- export const useForemanSettings = jest.fn(() => ({ perPage: 20 }));
2
- export const useForemanOrganization = jest.fn(() => ({
3
- id: 1,
4
- title: 'some-org',
5
- }));
6
- export const useForemanContext = jest.fn(() => ({ metadata: {} }));
@@ -1,6 +0,0 @@
1
- export { sprintf } from 'jed';
2
-
3
- export const translate = s => s;
4
-
5
- export const ngettext = (singular, plural, count) =>
6
- count === 1 ? singular : plural;
@@ -1,14 +0,0 @@
1
- import camelCase from 'lodash/camelCase';
2
-
3
- export const getURIQuery = jest.fn(() => ({}));
4
-
5
- export const noop = Function.prototype;
6
-
7
- export const propsToCamelCase = ob => {
8
- if (typeof ob !== 'object' || ob === null) return ob;
9
-
10
- return Object.keys(ob).reduce((memo, key) => {
11
- memo[camelCase(key)] = ob[key];
12
- return memo;
13
- }, {});
14
- };
@@ -1,24 +0,0 @@
1
- export const STATUS = {
2
- PENDING: 'PENDING',
3
- RESOLVED: 'RESOLVED',
4
- ERROR: 'ERROR',
5
- };
6
-
7
- export const getControllerSearchProps = (
8
- controller,
9
- id = 'searchBar',
10
- canCreateBookmarks = true
11
- ) => ({
12
- controller,
13
- autocomplete: {
14
- id,
15
- searchQuery: '',
16
- url: `${controller}/auto_complete_search`,
17
- useKeyShortcuts: true,
18
- },
19
- bookmarks: {
20
- url: '/api/bookmarks',
21
- canCreateBookmarks,
22
- documentationUrl: `4.1.5Searching`,
23
- },
24
- });
@@ -1,24 +0,0 @@
1
- import { STATUS } from '../../constants';
2
-
3
- export const selectAPI = state => state.API || {};
4
-
5
- export const selectAPIByKey = (state, key) => selectAPI(state)[key] || {};
6
-
7
- export const selectAPIStatus = (state, key) =>
8
- selectAPIByKey(state, key).status;
9
-
10
- export const selectAPIPayload = (state, key) =>
11
- selectAPIByKey(state, key).payload || {};
12
-
13
- export const selectAPIResponse = (state, key) =>
14
- selectAPIByKey(state, key).response || {};
15
-
16
- export const selectAPIError = (state, key) =>
17
- selectAPIStatus(state, key) === STATUS.ERROR
18
- ? selectAPIResponse(state, key)
19
- : null;
20
-
21
- export const selectAPIErrorMessage = (state, key) => {
22
- const error = selectAPIError(state, key);
23
- return error && error.message;
24
- };
@@ -1,12 +0,0 @@
1
- export const get = data => ({ type: 'get-some-type', ...data });
2
- export const put = data => ({ type: 'put-some-type', ...data });
3
- export const post = data => ({ type: 'post-some-type', ...data });
4
- export const patch = data => ({ type: 'patch-some-type', ...data });
5
-
6
- export const API = {
7
- get: jest.fn(),
8
- put: jest.fn(),
9
- post: jest.fn(),
10
- delete: jest.fn(),
11
- patch: jest.fn(),
12
- };