foreman_rh_cloud 5.0.32 → 5.0.35
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/rh_cloud/cloud_request_controller.rb +83 -0
- data/app/controllers/foreman_inventory_upload/uploads_controller.rb +7 -0
- data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +13 -2
- data/app/controllers/insights_cloud/hits_controller.rb +8 -1
- data/app/models/setting/rh_cloud.rb +2 -1
- data/app/services/foreman_rh_cloud/cloud_connector.rb +1 -1
- data/app/services/foreman_rh_cloud/cloud_presence.rb +124 -0
- data/app/services/foreman_rh_cloud/cloud_request.rb +8 -1
- data/app/services/foreman_rh_cloud/cloud_request_forwarder.rb +16 -5
- data/app/services/foreman_rh_cloud/hit_remediations_retriever.rb +67 -0
- data/app/services/foreman_rh_cloud/remediations_retriever.rb +16 -45
- data/app/services/foreman_rh_cloud/template_renderer_helper.rb +13 -1
- data/app/services/foreman_rh_cloud/url_remediations_retriever.rb +37 -0
- data/app/views/job_templates/cloud_connector.erb +30 -0
- data/app/views/job_templates/rh_cloud_download_playbook.erb +26 -0
- data/config/routes.rb +2 -0
- data/lib/foreman_rh_cloud/engine.rb +33 -12
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/lib/foreman_rh_cloud.rb +4 -0
- data/lib/insights_cloud/async/connector_playbook_execution_reporter_task.rb +193 -0
- data/lib/insights_cloud/async/insights_scheduled_sync.rb +4 -0
- data/lib/insights_cloud/generators/playbook_progress_generator.rb +49 -0
- data/lib/tasks/insights.rake +13 -0
- data/package.json +1 -1
- data/test/controllers/insights_cloud/api/cloud_request_controller_test.rb +78 -0
- data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +16 -0
- data/test/jobs/connector_playbook_execution_reporter_task_test.rb +207 -0
- data/test/unit/playbook_progress_generator_test.rb +75 -0
- data/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb +13 -8
- data/test/unit/services/foreman_rh_cloud/{remediations_retriever_test.rb → hit_remediations_retriever_test.rb} +3 -3
- data/test/unit/services/foreman_rh_cloud/url_remediations_retriever_test.rb +27 -0
- data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableActions.js +6 -11
- data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableHelpers.js +22 -0
- data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationActions.js +6 -1
- data/webpack/InsightsHostDetailsTab/InsightsTotalRiskChart.js +3 -2
- metadata +20 -4
@@ -6,15 +6,18 @@ module ForemanRhCloud
|
|
6
6
|
engine_name 'foreman_rh_cloud'
|
7
7
|
|
8
8
|
def self.register_scheduled_task(task_class, cronline)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
9
|
+
ForemanTasks::RecurringLogic.transaction(isolation: :serializable) do
|
10
|
+
return if ForemanTasks::RecurringLogic.joins(:tasks)
|
11
|
+
.merge(ForemanTasks::Task.where(label: task_class.name))
|
12
|
+
.exists?
|
13
|
+
|
14
|
+
User.as_anonymous_admin do
|
15
|
+
recurring_logic = ForemanTasks::RecurringLogic.new_from_cronline(cronline)
|
16
|
+
recurring_logic.save!
|
17
|
+
recurring_logic.start(task_class)
|
18
|
+
end
|
17
19
|
end
|
20
|
+
rescue ActiveRecord::TransactionIsolationError
|
18
21
|
end
|
19
22
|
|
20
23
|
initializer 'foreman_rh_cloud.load_default_settings', :before => :load_config_initializers do
|
@@ -76,9 +79,13 @@ module ForemanRhCloud
|
|
76
79
|
},
|
77
80
|
:resource_type => ::InsightsHit.name
|
78
81
|
)
|
82
|
+
permission(
|
83
|
+
:dispatch_cloud_requests,
|
84
|
+
'api/v2/rh_cloud/cloud_request': [:update]
|
85
|
+
)
|
79
86
|
end
|
80
87
|
|
81
|
-
plugin_permissions = [:view_foreman_rh_cloud, :generate_foreman_rh_cloud, :view_insights_hits]
|
88
|
+
plugin_permissions = [:view_foreman_rh_cloud, :generate_foreman_rh_cloud, :view_insights_hits, :dispatch_cloud_requests]
|
82
89
|
|
83
90
|
role 'ForemanRhCloud', plugin_permissions, 'Role granting permissions to view the hosts inventory,
|
84
91
|
generate a report, upload it to the cloud and download it locally'
|
@@ -116,7 +123,7 @@ module ForemanRhCloud
|
|
116
123
|
end
|
117
124
|
|
118
125
|
extend_template_helpers ForemanRhCloud::TemplateRendererHelper
|
119
|
-
allowed_template_helpers :remediations_playbook
|
126
|
+
allowed_template_helpers :remediations_playbook, :download_rh_playbook
|
120
127
|
end
|
121
128
|
|
122
129
|
::Katello::UINotifications::Subscriptions::ManifestImportSuccess.include ForemanInventoryUpload::Notifications::ManifestImportSuccessNotificationOverride if defined?(Katello)
|
@@ -136,15 +143,29 @@ module ForemanRhCloud
|
|
136
143
|
Foreman::Gettext::Support.add_text_domain locale_domain, locale_dir
|
137
144
|
end
|
138
145
|
|
139
|
-
|
146
|
+
initializer 'foreman_rh_cloud.register_rex_features', :before => :finisher_hook do |_app|
|
140
147
|
# skip database manipulations while tables do not exist, like in migrations
|
141
|
-
if ActiveRecord::Base.connection.data_source_exists?(ForemanTasks::Task.table_name)
|
148
|
+
if ActiveRecord::Base.connection.data_source_exists?(ForemanTasks::Task.table_name)
|
142
149
|
RemoteExecutionFeature.register(
|
143
150
|
:rh_cloud_remediate_hosts,
|
144
151
|
N_('Apply Insights recommendations'),
|
145
152
|
description: N_('Run remediation playbook generated by Insights'),
|
146
153
|
host_action_button: false
|
147
154
|
)
|
155
|
+
RemoteExecutionFeature.register(
|
156
|
+
:rh_cloud_connector_run_playbook,
|
157
|
+
N_('Run RH Cloud playbook'),
|
158
|
+
description: N_('Run playbook genrated by Red Hat remediations app'),
|
159
|
+
host_action_button: false,
|
160
|
+
provided_inputs: ['playbook_url', 'report_url', 'correlation_id', 'report_interval']
|
161
|
+
)
|
162
|
+
RemoteExecutionFeature.register(
|
163
|
+
:ansible_configure_cloud_connector,
|
164
|
+
N_('Configure Cloud Connector on given hosts'),
|
165
|
+
:description => N_('Configure Cloud Connector on given hosts'),
|
166
|
+
:proxy_selector_override => ::RemoteExecutionProxySelector::INTERNAL_PROXY
|
167
|
+
)
|
168
|
+
|
148
169
|
# skip object creation when admin user is not present, for example in test DB
|
149
170
|
if User.unscoped.find_by_login(User::ANONYMOUS_ADMIN).present?
|
150
171
|
::ForemanTasks.dynflow.config.on_init(false) do |world|
|
data/lib/foreman_rh_cloud.rb
CHANGED
@@ -121,4 +121,8 @@ module ForemanRhCloud
|
|
121
121
|
def self.legacy_insights_ca
|
122
122
|
"#{ForemanRhCloud::Engine.root}/config/rh_cert-api_chain.pem"
|
123
123
|
end
|
124
|
+
|
125
|
+
def self.cloud_url_validator
|
126
|
+
@cloud_url_validator ||= Regexp.new(ENV['SATELLITE_RH_CLOUD_VALIDATOR'] || 'redhat.com$')
|
127
|
+
end
|
124
128
|
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
module InsightsCloud
|
2
|
+
module Async
|
3
|
+
class ConnectorPlaybookExecutionReporterTask < ::Actions::EntryAction
|
4
|
+
include Dynflow::Action::Polling
|
5
|
+
include ForemanRhCloud::CloudAuth
|
6
|
+
|
7
|
+
def self.subscribe
|
8
|
+
Actions::RemoteExecution::RunHostsJob
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.connector_feature_id
|
12
|
+
@connector_feature_id ||= RemoteExecutionFeature.feature!(:rh_cloud_connector_run_playbook).id
|
13
|
+
end
|
14
|
+
|
15
|
+
def plan(job_invocation)
|
16
|
+
return unless connector_playbook_job?(job_invocation)
|
17
|
+
|
18
|
+
@job_invocation = job_invocation
|
19
|
+
|
20
|
+
invocation_inputs = invocation_inputs(job_invocation)
|
21
|
+
report_url = invocation_inputs['report_url']
|
22
|
+
report_interval = [invocation_inputs['report_interval'].to_i, 5].max
|
23
|
+
correlation_id = invocation_inputs['correlation_id']
|
24
|
+
|
25
|
+
plan_self(
|
26
|
+
report_url: report_url,
|
27
|
+
report_interval: report_interval,
|
28
|
+
job_invocation_id: job_invocation.id,
|
29
|
+
correlation_id: correlation_id
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def run(event = nil)
|
34
|
+
# Handle skip events
|
35
|
+
return if event == Dynflow::Action::Skip
|
36
|
+
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def rescue_strategy_for_self
|
41
|
+
Dynflow::Action::Rescue::Skip
|
42
|
+
end
|
43
|
+
|
44
|
+
def done?(current_status = invocation_status)
|
45
|
+
job_invocation.finished? || current_status.map { |_host_id, task_status| task_status['report_done'] }.all?
|
46
|
+
end
|
47
|
+
|
48
|
+
# noop, we don't want to do anything when the polling task starts
|
49
|
+
def invoke_external_task
|
50
|
+
poll_external_task
|
51
|
+
end
|
52
|
+
|
53
|
+
def poll_external_task
|
54
|
+
current_status = invocation_status
|
55
|
+
report_job_progress(current_status)
|
56
|
+
# record the current state and increment the sequence for the next invocation
|
57
|
+
{
|
58
|
+
invocation_status: current_status,
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def poll_intervals
|
63
|
+
[report_interval]
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def connector_playbook_job?(job_invocation)
|
69
|
+
puts "Job invocation id: #{job_invocation&.remote_execution_feature_id}, feature id: #{connector_feature_id}"
|
70
|
+
job_invocation&.remote_execution_feature_id == connector_feature_id
|
71
|
+
end
|
72
|
+
|
73
|
+
def connector_feature_id
|
74
|
+
self.class.connector_feature_id
|
75
|
+
end
|
76
|
+
|
77
|
+
def invocation_inputs(job_invocation)
|
78
|
+
Hash[
|
79
|
+
job_invocation.pattern_template_invocations.first.input_values.map do |input_value|
|
80
|
+
[input_value.template_input.name, input_value.value]
|
81
|
+
end
|
82
|
+
]
|
83
|
+
end
|
84
|
+
|
85
|
+
def job_invocation
|
86
|
+
@job_invocation ||= JobInvocation.find(input['job_invocation_id'])
|
87
|
+
end
|
88
|
+
|
89
|
+
def report_interval
|
90
|
+
@report_interval ||= input['report_interval']
|
91
|
+
end
|
92
|
+
|
93
|
+
def correlation_id
|
94
|
+
@correlation_id ||= input['correlation_id']
|
95
|
+
end
|
96
|
+
|
97
|
+
def host_status(host)
|
98
|
+
external_task&.dig('invocation_status', host)
|
99
|
+
end
|
100
|
+
|
101
|
+
def sequence(host)
|
102
|
+
host_status(host)&.fetch('sequence', 0).to_i
|
103
|
+
end
|
104
|
+
|
105
|
+
def host_done?(host)
|
106
|
+
ActiveModel::Type::Boolean.new.cast(host_status(host)&.fetch('report_done', nil))
|
107
|
+
end
|
108
|
+
|
109
|
+
def report_url
|
110
|
+
input['report_url']
|
111
|
+
end
|
112
|
+
|
113
|
+
def invocation_status
|
114
|
+
Hash[job_invocation.targeting.hosts.map do |host|
|
115
|
+
next unless host.insights&.uuid
|
116
|
+
[
|
117
|
+
host.insights.uuid,
|
118
|
+
task_status(job_invocation.sub_task_for_host(host), host.insights.uuid),
|
119
|
+
]
|
120
|
+
end.compact]
|
121
|
+
end
|
122
|
+
|
123
|
+
def task_status(host_task, host_name)
|
124
|
+
unless host_task
|
125
|
+
return { 'state' => 'unknown' }
|
126
|
+
end
|
127
|
+
|
128
|
+
{
|
129
|
+
'state' => host_task.state,
|
130
|
+
'output' => host_task.main_action.live_output.map { |line| line['output'] }.join("\n"),
|
131
|
+
'exit_status' => host_task.main_action.exit_status,
|
132
|
+
'sequence' => sequence(host_name),
|
133
|
+
'report_done' => host_done?(host_name),
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
def report_job_progress(invocation_status)
|
138
|
+
generator = InsightsCloud::Generators::PlaybookProgressGenerator.new(correlation_id)
|
139
|
+
|
140
|
+
invocation_status.each do |host_name, status|
|
141
|
+
# skip host if the host already reported that it's finished
|
142
|
+
next if status['report_done']
|
143
|
+
|
144
|
+
unless status['state'] == 'unknown'
|
145
|
+
sequence = status['sequence']
|
146
|
+
generator.host_progress_message(host_name, status['output'], sequence)
|
147
|
+
status['sequence'] = sequence + 1 # increase the sequence for each host
|
148
|
+
end
|
149
|
+
|
150
|
+
if status['state'] == 'stopped'
|
151
|
+
generator.host_finished_message(host_name, status['exit_status'])
|
152
|
+
status['report_done'] = true
|
153
|
+
end
|
154
|
+
end
|
155
|
+
generator.job_finished_message if done?(invocation_status)
|
156
|
+
|
157
|
+
send_report(generator.generate)
|
158
|
+
end
|
159
|
+
|
160
|
+
def send_report(report)
|
161
|
+
execute_cloud_request(
|
162
|
+
method: :post,
|
163
|
+
url: report_url,
|
164
|
+
content_type: 'application/vnd.redhat.playbook-sat.v3+jsonl',
|
165
|
+
payload: {
|
166
|
+
file: wrap_report(report),
|
167
|
+
multipart: true,
|
168
|
+
}
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
# RestClient has to accept an object that responds to :read, :path and :content_type methods
|
173
|
+
# to properly generate a multipart message.
|
174
|
+
# see: https://github.com/rest-client/rest-client/blob/2c72a2e77e2e87d25ff38feba0cf048d51bd5eca/lib/restclient/payload.rb#L161
|
175
|
+
def wrap_report(report)
|
176
|
+
obj = StringIO.new(report)
|
177
|
+
def obj.content_type
|
178
|
+
'application/vnd.redhat.playbook-sat.v3+jsonl'
|
179
|
+
end
|
180
|
+
|
181
|
+
def obj.path
|
182
|
+
'file'
|
183
|
+
end
|
184
|
+
|
185
|
+
obj
|
186
|
+
end
|
187
|
+
|
188
|
+
def logger
|
189
|
+
action_logger
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module InsightsCloud
|
2
|
+
module Generators
|
3
|
+
class PlaybookProgressGenerator
|
4
|
+
attr_reader :correlation_id
|
5
|
+
def initialize(correlation_id)
|
6
|
+
@messages = []
|
7
|
+
@correlation_id = correlation_id
|
8
|
+
end
|
9
|
+
|
10
|
+
def host_progress_message(host_name, output, sequence)
|
11
|
+
@messages << {
|
12
|
+
"type": "playbook_run_update",
|
13
|
+
"version": 3,
|
14
|
+
"correlation_id": correlation_id,
|
15
|
+
"sequence": sequence,
|
16
|
+
"host": host_name,
|
17
|
+
"console": output,
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def host_finished_message(host_name, exit_code)
|
22
|
+
@messages << {
|
23
|
+
"type": "playbook_run_finished",
|
24
|
+
"version": 3,
|
25
|
+
"correlation_id": correlation_id,
|
26
|
+
"host": host_name,
|
27
|
+
"status": exit_code == 0 ? 'success' : 'failure',
|
28
|
+
"connection_code": 0,
|
29
|
+
"execution_code": exit_code,
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def job_finished_message
|
34
|
+
@messages << {
|
35
|
+
"type": "playbook_run_completed",
|
36
|
+
"version": 3,
|
37
|
+
"correlation_id": correlation_id,
|
38
|
+
"status": "success",
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def generate
|
43
|
+
@messages.map do |message|
|
44
|
+
message.to_json
|
45
|
+
end.join("\n")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/tasks/insights.rake
CHANGED
@@ -19,4 +19,17 @@ namespace :rh_cloud_insights do
|
|
19
19
|
|
20
20
|
puts "Deleted #{deleted_count} insights statuses"
|
21
21
|
end
|
22
|
+
|
23
|
+
desc "Re-announce all organizations into Sources on RH cloud."
|
24
|
+
task announce_to_sources: [:environment] do
|
25
|
+
logger = Logging::Logger.new(STDOUT)
|
26
|
+
Organization.unscoped.each do |org|
|
27
|
+
presence = ForemanRhCloud::CloudPresence.new(org, logger)
|
28
|
+
presence.announce_to_sources
|
29
|
+
rescue StandardError => ex
|
30
|
+
logger.warn(ex)
|
31
|
+
end
|
32
|
+
|
33
|
+
logger.info('Reannounced all organizations')
|
34
|
+
end
|
22
35
|
end
|
data/package.json
CHANGED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'test_plugin_helper'
|
3
|
+
|
4
|
+
module InsightsCloud::Api
|
5
|
+
class CloudRequestControllerTest < ActionController::TestCase
|
6
|
+
tests Api::V2::RhCloud::CloudRequestController
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@test_org = FactoryBot.create(:organization)
|
10
|
+
end
|
11
|
+
|
12
|
+
test 'Fails for unknown directives' do
|
13
|
+
request_params = run_playbook_request
|
14
|
+
request_params['directive'] = 'not-valid'
|
15
|
+
|
16
|
+
post :update, params: request_params
|
17
|
+
|
18
|
+
assert_response :bad_request
|
19
|
+
end
|
20
|
+
|
21
|
+
test 'Starts playbook run for correct directive' do
|
22
|
+
Setting[:rh_cloud_token] = 'MOCK_TOKEN'
|
23
|
+
host1 = FactoryBot.create(:host, :with_insights_hits)
|
24
|
+
host1.insights.uuid = 'TEST_UUID1'
|
25
|
+
host1.insights.save!
|
26
|
+
host2 = FactoryBot.create(:host, :with_insights_hits)
|
27
|
+
host2.insights.uuid = 'TEST_UUID2'
|
28
|
+
host2.insights.save!
|
29
|
+
|
30
|
+
mock_composer = mock('composer')
|
31
|
+
::JobInvocationComposer.expects(:for_feature).with do |feature, host_ids, params|
|
32
|
+
feature == :rh_cloud_connector_run_playbook &&
|
33
|
+
host_ids.first == host1.id &&
|
34
|
+
host_ids.last == host2.id
|
35
|
+
end.returns(mock_composer)
|
36
|
+
mock_composer.expects(:trigger!)
|
37
|
+
mock_composer.expects(:job_invocation)
|
38
|
+
|
39
|
+
post :update, params: run_playbook_request
|
40
|
+
|
41
|
+
assert_response :success
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def run_playbook_request
|
47
|
+
request_json = <<-REQUEST
|
48
|
+
{
|
49
|
+
"type": "data",
|
50
|
+
"message_id": "a6a7d866-7de0-409a-84e0-3c56c4171bb7",
|
51
|
+
"version": 1,
|
52
|
+
"sent": "2021-01-12T15:30:08+00:00",
|
53
|
+
"directive": "playbook-sat",
|
54
|
+
"metadata": {
|
55
|
+
"operation": "run",
|
56
|
+
"return_url": "https://cloud.redhat.com/api/v1/ingres/upload",
|
57
|
+
"correlation_id": "6684b9dd-0d16-42c1-b13a-9f45be59e3b6",
|
58
|
+
"playbook_run_name": "Human-readable playbook run name",
|
59
|
+
"playbook_run_url": "https://console.redhat.com/insights/remediations/1234",
|
60
|
+
"sat_id": "aa3b1faa-56f3-4d14-8258-615d11e20060",
|
61
|
+
"sat_org_id": "#{FactoryBot.create(:organization).id}",
|
62
|
+
"initiator_user_id": "4efca34c6d9ae05ef7c3d7a7424e6370d198159a841ae005084888a9a4529e27",
|
63
|
+
"hosts": "TEST_UUID1,TEST_UUID2",
|
64
|
+
"response_interval": "30",
|
65
|
+
"response_full": "false"
|
66
|
+
},
|
67
|
+
"content": ""
|
68
|
+
}
|
69
|
+
REQUEST
|
70
|
+
|
71
|
+
request = JSON.parse(request_json)
|
72
|
+
|
73
|
+
request['content'] = "\"#{Base64.encode64('https://cloud.redhat.com/api/v1/remediations/1234/playbook')}\""
|
74
|
+
|
75
|
+
request
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -60,6 +60,18 @@ module InsightsCloud::Api
|
|
60
60
|
assert_equal x_rh_insights_request_id, @response.headers['x_rh_insights_request_id']
|
61
61
|
end
|
62
62
|
|
63
|
+
test "should set etag header to response from cloud" do
|
64
|
+
etag = '12345'
|
65
|
+
req = RestClient::Request.new(:method => 'GET', :url => 'http://test.theforeman.org', :headers => { "If-None-Match": etag})
|
66
|
+
net_http_resp = Net::HTTPResponse.new(1.0, 200, "OK")
|
67
|
+
net_http_resp[Rack::ETAG] = etag
|
68
|
+
res = RestClient::Response.create(@body, net_http_resp, req)
|
69
|
+
::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:forward_request).returns(res)
|
70
|
+
|
71
|
+
get :forward_request, params: { "path" => "static/v1/release/insights-core.egg" }
|
72
|
+
assert_equal etag, @response.headers[Rack::ETAG]
|
73
|
+
end
|
74
|
+
|
63
75
|
test "should handle failed authentication to cloud" do
|
64
76
|
net_http_resp = Net::HTTPResponse.new(1.0, 401, "Unauthorized")
|
65
77
|
res = RestClient::Response.create(@body, net_http_resp, @http_req)
|
@@ -116,6 +128,10 @@ module InsightsCloud::Api
|
|
116
128
|
test 'should get branch info' do
|
117
129
|
get :branch_info
|
118
130
|
|
131
|
+
res = JSON.parse(@response.body)
|
132
|
+
|
133
|
+
assert_not_equal 0, res['labels'].find { |l| l['key'] == 'hostgroup' }.count
|
134
|
+
|
119
135
|
assert_response :success
|
120
136
|
end
|
121
137
|
|