foreman_remote_execution 14.0.2 → 14.1.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +34 -17
  3. data/app/helpers/remote_execution_helper.rb +2 -2
  4. data/app/lib/actions/remote_execution/proxy_action.rb +10 -5
  5. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  6. data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +2 -3
  7. data/app/views/api/v2/job_invocations/hosts.json.rabl +15 -0
  8. data/config/routes.rb +1 -0
  9. data/db/migrate/20240312133027_extend_template_invocation_events.rb +19 -0
  10. data/lib/foreman_remote_execution/version.rb +1 -1
  11. data/webpack/JobInvocationDetail/JobInvocationActions.js +1 -1
  12. data/webpack/JobInvocationDetail/JobInvocationConstants.js +84 -0
  13. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +0 -1
  14. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +210 -0
  15. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +2 -2
  16. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +5 -1
  17. data/webpack/JobInvocationDetail/__tests__/fixtures.js +9 -0
  18. data/webpack/JobInvocationDetail/index.js +56 -34
  19. data/webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js +1 -2
  20. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +38 -7
  21. data/webpack/react_app/components/RecentJobsCard/constants.js +4 -0
  22. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +1 -1
  23. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +6 -6
  24. metadata +6 -94
  25. data/.babelrc.js +0 -3
  26. data/.eslintignore +0 -3
  27. data/.eslintrc +0 -13
  28. data/.github/workflows/js_ci.yml +0 -32
  29. data/.github/workflows/release.yml +0 -16
  30. data/.github/workflows/ruby_ci.yml +0 -19
  31. data/.gitignore +0 -19
  32. data/.packit.yaml +0 -45
  33. data/.prettierrc +0 -4
  34. data/.rubocop.yml +0 -105
  35. data/.rubocop_todo.yml +0 -516
  36. data/.tx/config +0 -10
  37. data/Gemfile +0 -5
  38. data/app/mailers/.gitkeep +0 -0
  39. data/app/views/dashboard/.gitkeep +0 -0
  40. data/foreman_remote_execution.gemspec +0 -33
  41. data/jsconfig.json +0 -8
  42. data/test/benchmark/run_hosts_job_benchmark.rb +0 -70
  43. data/test/benchmark/targeting_benchmark.rb +0 -31
  44. data/test/factories/foreman_remote_execution_factories.rb +0 -147
  45. data/test/functional/api/v2/foreign_input_sets_controller_test.rb +0 -58
  46. data/test/functional/api/v2/job_invocations_controller_test.rb +0 -446
  47. data/test/functional/api/v2/job_templates_controller_test.rb +0 -110
  48. data/test/functional/api/v2/registration_controller_test.rb +0 -73
  49. data/test/functional/api/v2/remote_execution_features_controller_test.rb +0 -34
  50. data/test/functional/api/v2/template_invocations_controller_test.rb +0 -33
  51. data/test/functional/cockpit_controller_test.rb +0 -16
  52. data/test/functional/job_invocations_controller_test.rb +0 -132
  53. data/test/functional/job_templates_controller_test.rb +0 -31
  54. data/test/functional/ui_job_wizard_controller_test.rb +0 -16
  55. data/test/graphql/mutations/job_invocations/create_test.rb +0 -58
  56. data/test/graphql/queries/job_invocation_query_test.rb +0 -31
  57. data/test/graphql/queries/job_invocations_query_test.rb +0 -35
  58. data/test/helpers/remote_execution_helper_test.rb +0 -46
  59. data/test/support/remote_execution_helper.rb +0 -5
  60. data/test/test_plugin_helper.rb +0 -9
  61. data/test/unit/actions/run_host_job_test.rb +0 -115
  62. data/test/unit/actions/run_hosts_job_test.rb +0 -214
  63. data/test/unit/api_params_test.rb +0 -25
  64. data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +0 -29
  65. data/test/unit/concerns/host_extensions_test.rb +0 -219
  66. data/test/unit/concerns/nic_extensions_test.rb +0 -9
  67. data/test/unit/execution_task_status_mapper_test.rb +0 -92
  68. data/test/unit/input_template_renderer_test.rb +0 -503
  69. data/test/unit/job_invocation_composer_test.rb +0 -974
  70. data/test/unit/job_invocation_report_template_test.rb +0 -60
  71. data/test/unit/job_invocation_test.rb +0 -232
  72. data/test/unit/job_template_effective_user_test.rb +0 -37
  73. data/test/unit/job_template_test.rb +0 -316
  74. data/test/unit/remote_execution_feature_test.rb +0 -86
  75. data/test/unit/remote_execution_provider_test.rb +0 -298
  76. data/test/unit/renderer_scope_input_test.rb +0 -49
  77. data/test/unit/targeting_test.rb +0 -206
  78. data/test/unit/template_invocation_input_value_test.rb +0 -38
@@ -1,974 +0,0 @@
1
- require 'test_plugin_helper'
2
- RemoteExecutionProvider.register(:Ansible, OpenStruct)
3
- RemoteExecutionProvider.register(:Mcollective, OpenStruct)
4
-
5
- class JobInvocationComposerTest < ActiveSupport::TestCase
6
- before do
7
- setup_user('create', 'template_invocations')
8
- setup_user('view', 'job_templates', 'name ~ trying*')
9
- setup_user('create', 'job_templates', 'name ~ trying*')
10
- setup_user('view', 'job_invocations')
11
- setup_user('create', 'job_invocations')
12
- setup_user('view', 'bookmarks')
13
- setup_user('create', 'bookmarks')
14
- setup_user('edit', 'bookmarks')
15
- setup_user('view', 'hosts')
16
- setup_user('create', 'hosts')
17
- end
18
-
19
- class AnsibleInputs < RemoteExecutionProvider
20
- class << self
21
- def provider_input_namespace
22
- :ansible
23
- end
24
-
25
- def provider_inputs
26
- [
27
- ForemanRemoteExecution::ProviderInput.new(name: 'tags', label: 'Tags', value: 'fooo', value_type: 'plain'),
28
- ForemanRemoteExecution::ProviderInput.new(name: 'tags_flag', label: 'Tags Flag', value: '--tags', options: "--tags\n--skip-tags"),
29
- ]
30
- end
31
- end
32
- end
33
- RemoteExecutionProvider.register(:AnsibleInputs, AnsibleInputs)
34
-
35
- let(:trying_job_template_1) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying1', :provider_type => 'SSH') }
36
- let(:trying_job_template_2) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_2', :name => 'trying2', :provider_type => 'Mcollective') }
37
- let(:trying_job_template_3) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying3', :provider_type => 'SSH') }
38
- let(:trying_job_template_5) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_5', :name => 'trying5', :provider_type => 'SSH') }
39
- let(:unauthorized_job_template_1) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'unauth1', :provider_type => 'SSH') }
40
- let(:unauthorized_job_template_2) { FactoryBot.create(:job_template, :job_category => 'unauthorized_job_template_2', :name => 'unauth2', :provider_type => 'Ansible') }
41
-
42
- let(:provider_inputs_job_template) { FactoryBot.create(:job_template, :job_category => 'trying_test_inputs', :name => 'trying provider inputs', :provider_type => 'AnsibleInputs') }
43
-
44
- let(:input1) { FactoryBot.create(:template_input, :template => trying_job_template_1, :input_type => 'user') }
45
- let(:input2) { FactoryBot.create(:template_input, :template => trying_job_template_3, :input_type => 'user') }
46
- let(:input3) { FactoryBot.create(:template_input, :template => trying_job_template_1, :input_type => 'user', :required => true) }
47
- let(:input4) { FactoryBot.create(:template_input, :template => provider_inputs_job_template, :input_type => 'user') }
48
- let(:input5) { FactoryBot.create(:template_input, :template => trying_job_template_5, :input_type => 'user', :default => 'value') }
49
- let(:unauthorized_input1) { FactoryBot.create(:template_input, :template => unauthorized_job_template_1, :input_type => 'user') }
50
-
51
- let(:ansible_params) { { } }
52
- let(:ssh_params) { { } }
53
- let(:mcollective_params) { { } }
54
- let(:providers_params) { { :providers => { :ansible => ansible_params, :ssh => ssh_params, :mcollective => mcollective_params } } }
55
-
56
- context 'with general new invocation and empty params' do
57
- let(:params) { {} }
58
- let(:composer) { JobInvocationComposer.from_ui_params(params) }
59
-
60
- describe '#available_templates' do
61
- it 'obeys authorization' do
62
- composer # lazy load composer before stubbing
63
- JobTemplate.expects(:authorized).with(:view_job_templates).returns(JobTemplate.where({}))
64
- composer.available_templates
65
- end
66
- end
67
-
68
- context 'job templates exist' do
69
- before do
70
- trying_job_template_1
71
- trying_job_template_2
72
- trying_job_template_3
73
- unauthorized_job_template_1
74
- unauthorized_job_template_2
75
- end
76
-
77
- describe '#available_templates_for(job_category)' do
78
- it 'find the templates only for a given job name' do
79
- results = composer.available_templates_for(trying_job_template_1.job_category)
80
- assert_includes results, trying_job_template_1
81
- refute_includes results, trying_job_template_2
82
- end
83
-
84
- it 'it respects view permissions' do
85
- results = composer.available_templates_for(trying_job_template_1.job_category)
86
- refute_includes results, unauthorized_job_template_1
87
- end
88
- end
89
-
90
- describe '#available_job_categories' do
91
- let(:job_categories) { composer.available_job_categories }
92
-
93
- it 'find only job names that user is granted to view' do
94
- assert_includes job_categories, trying_job_template_1.job_category
95
- assert_includes job_categories, trying_job_template_2.job_category
96
- refute_includes job_categories, unauthorized_job_template_2.job_category
97
- end
98
-
99
- it 'every job name is listed just once' do
100
- assert_equal job_categories.uniq, job_categories
101
- end
102
- end
103
-
104
- describe '#available_provider_types' do
105
- let(:provider_types) { composer.available_provider_types }
106
-
107
- it 'finds only providers which user is granted to view' do
108
- composer.job_invocation.job_category = 'trying_job_template_1'
109
- assert_includes provider_types, 'SSH'
110
- refute_includes provider_types, 'Mcollective'
111
- refute_includes provider_types, 'Ansible'
112
- end
113
-
114
- it 'every provider type is listed just once' do
115
- assert_equal provider_types.uniq, provider_types
116
- end
117
- end
118
-
119
- describe '#available_template_inputs' do
120
- before do
121
- input1
122
- input2
123
- unauthorized_input1
124
- end
125
-
126
- it 'returns only authorized inputs based on templates' do
127
- assert_includes composer.available_template_inputs, input1
128
- assert_includes composer.available_template_inputs, input2
129
- refute_includes composer.available_template_inputs, unauthorized_input1
130
- end
131
-
132
- context 'params contains job template ids' do
133
- let(:ssh_params) { { :job_template_id => trying_job_template_1.id.to_s } }
134
- let(:ansible_params) { { :job_template_id => '' } }
135
- let(:mcollective_params) { { :job_template_id => '' } }
136
- let(:params) { { :job_invocation => providers_params }.with_indifferent_access }
137
-
138
- it 'finds the inputs only specified job templates' do
139
- assert_includes composer.available_template_inputs, input1
140
- refute_includes composer.available_template_inputs, input2
141
- refute_includes composer.available_template_inputs, unauthorized_input1
142
- end
143
- end
144
- end
145
-
146
- describe '#needs_provider_type_selection?' do
147
- it 'returns true if there are more than one providers respecting authorization' do
148
- composer.stubs(:available_provider_types => [ 'SSH', 'Ansible' ])
149
- assert composer.needs_provider_type_selection?
150
- end
151
-
152
- it 'returns false if there is one provider' do
153
- composer.stubs(:available_provider_types => [ 'SSH' ])
154
- refute composer.needs_provider_type_selection?
155
- end
156
- end
157
-
158
- describe '#displayed_provider_types' do
159
- # nothing to test yet
160
- end
161
-
162
- describe '#templates_for_provider(provider_type)' do
163
- it 'returns all templates for a given provider respecting template permissions' do
164
- trying_job_template_4 = FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying4', :provider_type => 'Ansible')
165
- result = composer.templates_for_provider('SSH')
166
- assert_includes result, trying_job_template_1
167
- assert_includes result, trying_job_template_3
168
- refute_includes result, unauthorized_job_template_1
169
- refute_includes result, trying_job_template_4
170
-
171
- result = composer.templates_for_provider('Ansible')
172
- refute_includes result, trying_job_template_1
173
- refute_includes result, trying_job_template_3
174
- refute_includes result, unauthorized_job_template_2
175
- assert_includes result, trying_job_template_4
176
- end
177
- end
178
-
179
- describe '#rerun_possible?' do
180
- it 'is true when not rerunning' do
181
- assert_predicate composer, :rerun_possible?
182
- end
183
-
184
- it 'is true when rerunning with pattern tempalte invocations' do
185
- composer.expects(:reruns).returns(1)
186
- composer.job_invocation.expects(:pattern_template_invocations).returns([1])
187
- assert_predicate composer, :rerun_possible?
188
- end
189
-
190
- it 'is false when rerunning without pattern template invocations' do
191
- composer.expects(:reruns).returns(1)
192
- composer.job_invocation.expects(:pattern_template_invocations).returns([])
193
- refute_predicate composer, :rerun_possible?
194
- end
195
- end
196
-
197
- describe '#selected_job_templates' do
198
- it 'returns no template if none was selected through params' do
199
- assert_empty composer.selected_job_templates
200
- end
201
-
202
- context 'extra unavailable templates id were selected' do
203
- let(:unauthorized) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'unauth3', :provider_type => 'Ansible') }
204
- let(:mcollective_authorized) { FactoryBot.create(:job_template, :job_category => 'trying_job_template_1', :name => 'trying4', :provider_type => 'Mcollective') }
205
- let(:ssh_params) { { :job_template_id => trying_job_template_1.id.to_s } }
206
- let(:ansible_params) { { :job_template_id => unauthorized.id.to_s } }
207
- let(:mcollective_params) { { :job_template_id => mcollective_authorized.id.to_s } }
208
- let(:params) { { :job_invocation => providers_params }.with_indifferent_access }
209
-
210
- it 'ignores unauthorized template' do
211
- unauthorized # make sure unautorized exists
212
- refute_includes composer.selected_job_templates, unauthorized
213
- end
214
-
215
- it 'contains only authorized template specified in params' do
216
- mcollective_authorized # make sure mcollective_authorized exists
217
- assert_includes composer.selected_job_templates, trying_job_template_1
218
- assert_includes composer.selected_job_templates, mcollective_authorized
219
- refute_includes composer.selected_job_templates, trying_job_template_3
220
- end
221
- end
222
- end
223
-
224
- describe '#preselected_template_for_provider(provider_type)' do
225
- context 'none template was selected through params' do
226
- it 'returns nil' do
227
- assert_nil composer.preselected_template_for_provider('SSH')
228
- end
229
- end
230
-
231
- context 'available template was selected for a specified provider through params' do
232
- let(:ssh_params) { { :job_template_id => trying_job_template_1.id.to_s } }
233
- let(:params) { { :job_invocation => providers_params }.with_indifferent_access }
234
-
235
- it 'returns the selected template because it is available for provider' do
236
- assert_equal trying_job_template_1, composer.preselected_template_for_provider('SSH')
237
- end
238
- end
239
- end
240
-
241
- describe '#pattern template_invocations' do
242
- let(:ssh_params) do
243
- { :job_template_id => trying_job_template_1.id.to_s,
244
- :job_templates => {
245
- trying_job_template_1.id.to_s => {
246
- :input_values => { input1.id.to_s => { :value => 'value1' }, unauthorized_input1.id.to_s => { :value => 'dropped' } },
247
- },
248
- }}
249
- end
250
- let(:params) { { :job_invocation => { :providers => { :ssh => ssh_params } } }.with_indifferent_access }
251
- let(:invocations) { composer.pattern_template_invocations }
252
-
253
- it 'builds pattern template invocations based on passed params and it filters out wrong inputs' do
254
- assert_equal 1, invocations.size
255
- assert_equal 1, invocations.first.input_values.size
256
- assert_equal 'value1', invocations.first.input_values.first.value
257
- end
258
- end
259
-
260
- describe '#effective_user' do
261
- let(:ssh_params) do
262
- { :job_template_id => trying_job_template_1.id.to_s,
263
- :job_templates => {
264
- trying_job_template_1.id.to_s => {
265
- :effective_user => invocation_effective_user,
266
- },
267
- }}
268
- end
269
- let(:params) { { :job_invocation => { :providers => { :ssh => ssh_params } } }.with_indifferent_access }
270
- let(:template_invocation) do
271
- trying_job_template_1.effective_user.update(:overridable => overridable, :value => 'template user')
272
- composer.pattern_template_invocations.first
273
- end
274
-
275
- context 'when overridable and provided' do
276
- let(:overridable) { true }
277
- let(:invocation_effective_user) { 'invocation user' }
278
-
279
- it 'takes the value from the template invocation' do
280
- assert_equal 'invocation user', template_invocation.effective_user
281
- end
282
- end
283
-
284
- context 'when overridable and not provided' do
285
- let(:overridable) { true }
286
- let(:invocation_effective_user) { '' }
287
-
288
- it 'takes the value from the job template' do
289
- assert_equal 'template user', template_invocation.effective_user
290
- end
291
- end
292
-
293
- context 'when not overridable and provided' do
294
- let(:overridable) { false }
295
- let(:invocation_effective_user) { 'invocation user' }
296
-
297
- it 'takes the value from the job template' do
298
- assert_equal 'template user', template_invocation.effective_user
299
- end
300
- end
301
- end
302
-
303
- describe '#displayed_search_query' do
304
- it 'is empty by default' do
305
- assert_empty composer.displayed_search_query
306
- end
307
-
308
- let(:host) { FactoryBot.create(:host) }
309
- let(:bookmark) { Bookmark.create!(:query => 'b', :name => 'bookmark', :public => true, :controller => 'hosts') }
310
-
311
- context 'all targetings parameters are present' do
312
- let(:params) { { :targeting => { :search_query => 'a', :bookmark_id => bookmark.id }, :host_ids => [ host.id ] }.with_indifferent_access }
313
-
314
- it 'explicit search query has highest priority' do
315
- assert_equal 'a', composer.displayed_search_query
316
- end
317
- end
318
-
319
- context 'host ids and bookmark are present' do
320
- let(:params) { { :targeting => { :bookmark_id => bookmark.id }, :host_ids => [ host.id ] }.with_indifferent_access }
321
-
322
- it 'hosts will be used instead of a bookmark' do
323
- assert_includes composer.displayed_search_query, host.name
324
- end
325
- end
326
-
327
- context 'bookmark is present' do
328
- let(:params) { { :targeting => { :bookmark_id => bookmark.id } }.with_indifferent_access }
329
-
330
- it 'bookmark query is used if it is available for the user' do
331
- bookmark.update_attribute :public, false
332
- assert_equal bookmark.query, composer.displayed_search_query
333
- end
334
-
335
- it 'bookmark query is used if the bookmark is public' do
336
- bookmark.owner = nil
337
- bookmark.save(:validate => false) # skip validations so owner remains nil
338
- assert_equal bookmark.query, composer.displayed_search_query
339
- end
340
-
341
- it 'empty search is returned if bookmark is not owned by the user and is not public' do
342
- bookmark.public = false
343
- bookmark.owner = nil
344
- bookmark.save(:validate => false) # skip validations so owner remains nil
345
- assert_empty composer.displayed_search_query
346
- end
347
- end
348
- end
349
-
350
- describe '#available_bookmarks' do
351
- context 'there are hostgroups and hosts bookmark' do
352
- let(:hostgroups) { Bookmark.create(:name => 'hostgroups', :query => 'name = x', :controller => 'hostgroups') }
353
- let(:hosts) { Bookmark.create(:name => 'hosts', :query => 'name = x', :controller => 'hosts') }
354
- let(:dashboard) { Bookmark.create(:name => 'dashboard', :query => 'name = x', :controller => 'dashboard') }
355
-
356
- it 'finds only host related bookmarks' do
357
- hosts
358
- dashboard
359
- hostgroups
360
- assert_includes composer.available_bookmarks, hosts
361
- assert_includes composer.available_bookmarks, dashboard
362
- refute_includes composer.available_bookmarks, hostgroups
363
- end
364
- end
365
- end
366
-
367
- describe '#targeted_hosts_count' do
368
- let(:host) { FactoryBot.create(:host) }
369
-
370
- it 'obeys authorization' do
371
- fake_scope = mock
372
- composer.stubs(:displayed_search_query => "name = #{host.name}")
373
- Host.expects(:execution_scope).returns(fake_scope)
374
- fake_scope.expects(:authorized).with(:view_hosts, Host).returns(Host.where({}))
375
- composer.targeted_hosts_count
376
- end
377
-
378
- it 'searches hosts based on displayed_search_query' do
379
- composer.stubs(:displayed_search_query => "name = #{host.name}")
380
- assert_equal 1, composer.targeted_hosts_count
381
- end
382
-
383
- it 'returns 0 for queries with syntax errors' do
384
- composer.stubs(:displayed_search_query => 'name = ')
385
- assert_equal 0, composer.targeted_hosts_count
386
- end
387
-
388
- it 'returns 0 when no query is present' do
389
- composer.stubs(:displayed_search_query => '')
390
- assert_equal 0, composer.targeted_hosts_count
391
- end
392
- end
393
-
394
- describe '#input_value_for(input)' do
395
- let(:value1) { composer.input_value_for(input1) }
396
- it 'returns new empty input value if there is no invocation' do
397
- assert value1.new_record?
398
- assert_empty value1.value
399
- end
400
-
401
- context 'there are invocations without input values for a given input' do
402
- let(:ssh_params) do
403
- { :job_template_id => trying_job_template_1.id.to_s,
404
- :job_templates => {
405
- trying_job_template_1.id.to_s => {
406
- :input_values => { },
407
- },
408
- } }
409
- end
410
- let(:params) { { :job_invocation => { :providers => { :ssh => ssh_params } } }.with_indifferent_access }
411
-
412
- it 'returns new empty input value' do
413
- assert value1.new_record?
414
- assert_empty value1.value
415
- end
416
- end
417
-
418
- context 'there are invocations with input values for a given input' do
419
- let(:ssh_params) do
420
- { :job_template_id => trying_job_template_1.id.to_s,
421
- :job_templates => {
422
- trying_job_template_1.id.to_s => {
423
- :input_values => { input1.id.to_s => { :value => 'value1' } },
424
- },
425
- } }
426
- end
427
- let(:params) { { :job_invocation => { :providers => { :ssh => ssh_params } } }.with_indifferent_access }
428
-
429
- it 'finds the value among template invocations' do
430
- assert_equal 'value1', value1.value
431
- end
432
- end
433
- end
434
-
435
- describe '#valid?' do
436
- let(:host) { FactoryBot.create(:host) }
437
- let(:ssh_params) do
438
- { :job_template_id => trying_job_template_1.id.to_s,
439
- :job_templates => {
440
- trying_job_template_1.id.to_s => {
441
- :input_values => { input1.id.to_s => { :value => 'value1' } },
442
- },
443
- } }
444
- end
445
-
446
- let(:params) do
447
- { :job_invocation => { :providers => { :ssh => ssh_params } }, :targeting => { :search_query => "name = #{host.name}" } }.with_indifferent_access
448
- end
449
-
450
- it 'validates all associated objects even if some of the is invalid' do
451
- composer
452
- composer.job_invocation.expects(:valid?).returns(false)
453
- composer.targeting.expects(:valid?).returns(false)
454
- composer.pattern_template_invocations.each { |invocation| invocation.expects(:valid?).returns(false) }
455
- assert_not composer.valid?
456
- end
457
- end
458
-
459
- describe 'concurrency control' do
460
-
461
- describe 'with concurrency control set' do
462
- let(:params) do
463
- { :job_invocation => { :providers => { :ssh => ssh_params }, :concurrency_level => '5' } }.with_indifferent_access
464
- end
465
-
466
- it 'accepts the concurrency options' do
467
- assert_equal 5, composer.job_invocation.concurrency_level
468
- end
469
- end
470
-
471
- it 'can be disabled' do
472
- assert_nil composer.job_invocation.concurrency_level
473
- end
474
- end
475
-
476
- describe 'triggering' do
477
- let(:params) do
478
- { :job_invocation => { :providers => { :ssh => ssh_params } }, :triggering => { :mode => 'future', :end_time=> {"end_time(3i)": 1, "end_time(2i)": 2, "end_time(1i)": 3, "end_time(4i)": 4, "end_time(5i)": 5} }}.with_indifferent_access
479
- end
480
-
481
- it 'accepts the triggering params' do
482
- assert_equal :future, composer.job_invocation.triggering.mode
483
- end
484
-
485
- it 'formats the triggering end time when its unordered' do
486
- assert_equal Time.local(3,2,1,4,5), composer.job_invocation.triggering.end_time
487
- end
488
- end
489
-
490
- describe '#save' do
491
- it 'triggers save on job_invocation if it is valid' do
492
- composer.stubs(:valid? => true)
493
- composer.job_invocation.expects(:save)
494
- composer.save
495
- end
496
-
497
- it 'does not trigger save on job_invocation if it is invalid' do
498
- composer.stubs(:valid? => false)
499
- composer.job_invocation.expects(:save).never
500
- composer.save
501
- end
502
- end
503
-
504
- describe '#job_category' do
505
- it 'triggers job_category on job_invocation' do
506
- composer
507
- composer.job_invocation.expects(:job_category)
508
- composer.job_category
509
- end
510
- end
511
-
512
- describe '#password' do
513
- let(:password) { 'changeme' }
514
- let(:params) do
515
- { :job_invocation => { :password => password }}.with_indifferent_access
516
- end
517
-
518
- it 'sets the password properly' do
519
- composer
520
- assert_equal password, composer.job_invocation.password
521
- end
522
- end
523
-
524
- describe '#key_passphrase' do
525
- let(:key_passphrase) { 'changeme' }
526
- let(:params) do
527
- { :job_invocation => { :key_passphrase => key_passphrase }}
528
- end
529
-
530
- it 'sets the key passphrase properly' do
531
- composer
532
- assert_equal key_passphrase, composer.job_invocation.key_passphrase
533
- end
534
- end
535
-
536
- describe '#effective_user_password' do
537
- let(:effective_user_password) { 'password' }
538
- let(:params) do
539
- { :job_invocation => { :effective_user_password => effective_user_password }}
540
- end
541
-
542
- it 'sets the effective_user_password password properly' do
543
- composer
544
- assert_equal effective_user_password, composer.job_invocation.effective_user_password
545
- end
546
- end
547
-
548
- describe '#targeting' do
549
- it 'triggers targeting on job_invocation' do
550
- composer
551
- composer.job_invocation.expects(:targeting)
552
- composer.targeting
553
- end
554
- end
555
-
556
- describe '#compose_from_invocation(existing_invocation)' do
557
- let(:host) { FactoryBot.create(:host) }
558
- let(:ssh_params) do
559
- { :job_template_id => trying_job_template_1.id.to_s,
560
- :job_templates => {
561
- trying_job_template_1.id.to_s => {
562
- :input_values => { input1.id.to_s => { :value => 'value1' } },
563
- },
564
- } }
565
- end
566
- let(:params) do
567
- {
568
- :job_invocation => {
569
- :providers => { :ssh => ssh_params },
570
- :concurrency_level => 5,
571
- },
572
- :targeting => {
573
- :search_query => "name = #{host.name}",
574
- :targeting_type => Targeting::STATIC_TYPE,
575
- },
576
- }.with_indifferent_access
577
- end
578
- let(:existing) { composer.job_invocation }
579
- let(:new_composer) { JobInvocationComposer.from_job_invocation(composer.job_invocation) }
580
-
581
- before do
582
- composer.save
583
- end
584
-
585
- it 'sets the same job name' do
586
- assert_equal existing.job_category, new_composer.job_category
587
- end
588
-
589
- it 'accepts additional host ids' do
590
- new_composer = JobInvocationComposer.from_job_invocation(composer.job_invocation, { :host_ids => [host.id] })
591
- assert_equal "name ^ (#{host.name})", new_composer.search_query
592
- end
593
-
594
- it 'builds new targeting object which keeps search query' do
595
- refute_equal existing.targeting, new_composer.targeting
596
- assert_equal existing.targeting.search_query, new_composer.search_query
597
- end
598
-
599
- it 'keeps job template ids' do
600
- assert_equal existing.pattern_template_invocations.map(&:template_id), new_composer.job_template_ids
601
- end
602
-
603
- it 'keeps template invocations and their values' do
604
- assert_equal existing.pattern_template_invocations.size, new_composer.pattern_template_invocations.size
605
- end
606
-
607
- it 'sets the same concurrency control options' do
608
- assert_equal existing.concurrency_level, new_composer.job_invocation.concurrency_level
609
- end
610
-
611
- end
612
- end
613
- end
614
-
615
- describe '.from_api_params' do
616
- let(:composer) do
617
- JobInvocationComposer.from_api_params(params)
618
- end
619
- let(:bookmark) { bookmarks(:one) }
620
-
621
- context 'with targeting from bookmark' do
622
-
623
- before do
624
- [trying_job_template_1, trying_job_template_3] # mentioning templates we want to have initialized in the test
625
- end
626
-
627
- let(:params) do
628
- { :job_category => trying_job_template_1.job_category,
629
- :job_template_id => trying_job_template_1.id,
630
- :targeting_type => 'static_query',
631
- :bookmark_id => bookmark.id }
632
- end
633
-
634
- it 'creates invocation with a bookmark' do
635
- assert composer.save!
636
- assert_equal bookmark, composer.job_invocation.targeting.bookmark
637
- assert_equal composer.job_invocation.targeting.user, User.current
638
- assert_not_empty composer.job_invocation.pattern_template_invocations
639
- end
640
- end
641
-
642
- context 'with targeting from search query' do
643
- let(:params) do
644
- { :job_category => trying_job_template_1.job_category,
645
- :job_template_id => trying_job_template_1.id,
646
- :targeting_type => 'static_query',
647
- :search_query => 'some hosts' }
648
- end
649
-
650
- it 'creates invocation with a search query' do
651
- assert composer.save!
652
- assert_equal 'some hosts', composer.job_invocation.targeting.search_query
653
- assert_not_empty composer.job_invocation.pattern_template_invocations
654
- end
655
- end
656
-
657
- context 'with with inputs' do
658
- let(:params) do
659
- { :job_category => trying_job_template_5.job_category,
660
- :job_template_id => trying_job_template_5.id,
661
- :targeting_type => 'static_query',
662
- :search_query => 'some hosts',
663
- :inputs => {input5.name => 'some_value'}}
664
- end
665
-
666
- it 'finds the inputs by name' do
667
- assert composer.save!
668
- values = composer.pattern_template_invocations.first.input_values
669
- assert_equal 1, values.count
670
- assert_equal 'some_value', values.first.value
671
- end
672
-
673
- it 'can be forced to be empty' do
674
- params[:inputs] = {input5.name => ''}
675
- assert composer.save!
676
- values = composer.pattern_template_invocations.first.input_values
677
- assert_equal 1, values.count
678
- assert_equal '', values.first.value
679
- end
680
- end
681
-
682
- context 'with inputs and default values' do
683
- let(:params) do
684
- { :job_category => trying_job_template_5.job_category,
685
- :job_template_id => trying_job_template_5.id,
686
- :targeting_type => 'static_query',
687
- :search_query => 'some hosts',
688
- :inputs => {}}
689
- end
690
-
691
- it 'uses the default input values' do
692
- input5 # Force the factory to be materialized
693
- assert composer.save!
694
- assert_equal 1, composer.pattern_template_invocations.first.input_values.collect.count
695
- end
696
- end
697
-
698
- context 'with provider inputs' do
699
- let(:params) do
700
- { :job_category => provider_inputs_job_template.job_category,
701
- :job_template_id => provider_inputs_job_template.id,
702
- :targeting_type => 'static_query',
703
- :search_query => 'some hosts',
704
- :inputs => { input4.name => 'some_value' },
705
- :ansible => { 'tags' => 'bar', 'tags_flag' => '--skip-tags' } }
706
- end
707
-
708
- it 'detects provider inputs' do
709
- assert composer.save!
710
- scope = composer.job_invocation.pattern_template_invocations.first.provider_input_values
711
- tags = scope.find_by :name => 'tags'
712
- flags = scope.find_by :name => 'tags_flag'
713
- assert_equal 'bar', tags.value
714
- assert_equal '--skip-tags', flags.value
715
- end
716
- end
717
-
718
- context 'with effective user' do
719
- let(:params) do
720
- { :job_category => trying_job_template_1.job_category,
721
- :job_template_id => trying_job_template_1.id,
722
- :effective_user => 'invocation user',
723
- :targeting_type => 'static_query',
724
- :search_query => 'some hosts',
725
- :inputs => {input1.name => 'some_value'}}
726
- end
727
-
728
- let(:template_invocation) { composer.job_invocation.pattern_template_invocations.first }
729
-
730
- it 'sets the effective user based on the input' do
731
- assert composer.save!
732
- assert_equal 'invocation user', template_invocation.effective_user
733
- end
734
- end
735
-
736
- context 'with concurrency_control' do
737
- let(:level) { 5 }
738
- let(:params) do
739
- { :job_category => trying_job_template_1.job_category,
740
- :job_template_id => trying_job_template_1.id,
741
- :concurrency_control => {
742
- :concurrency_level => level,
743
- },
744
- :targeting_type => 'static_query',
745
- :search_query => 'some hosts',
746
- :inputs => { input1.name => 'some_value' } }
747
- end
748
-
749
- it 'sets the concurrency level based on the input' do
750
- assert composer.save!
751
- assert_equal level, composer.job_invocation.concurrency_level
752
- end
753
- end
754
-
755
- context 'with rex feature defined' do
756
- let(:feature) { FactoryBot.create(:remote_execution_feature) }
757
- let(:params) do
758
- { :job_category => trying_job_template_1.job_category,
759
- :job_template_id => trying_job_template_1.id,
760
- :remote_execution_feature_id => feature.id,
761
- :targeting_type => 'static_query',
762
- :search_query => 'some hosts',
763
- :inputs => { input1.name => 'some_value' } }
764
- end
765
-
766
- it 'sets the remote execution feature based on the input' do
767
- assert composer.save!
768
- assert_equal feature, composer.job_invocation.remote_execution_feature
769
- end
770
-
771
- it 'sets the remote execution_feature id based on `feature` param' do
772
- params[:remote_execution_feature_id] = nil
773
- params[:feature] = feature.label
774
- params[:job_template_id] = trying_job_template_1.id
775
- refute_equal feature.job_template, trying_job_template_1
776
-
777
- assert composer.save!
778
- assert_equal feature, composer.job_invocation.remote_execution_feature
779
- end
780
- end
781
-
782
- context 'with invalid targeting' do
783
- let(:params) do
784
- { :job_category => trying_job_template_1.job_category,
785
- :job_template_id => trying_job_template_1.id,
786
- :targeting_type => 'fake',
787
- :search_query => 'some hosts',
788
- :inputs => {input1.name => 'some_value'}}
789
- end
790
-
791
- it 'handles errors' do
792
- assert_raises(ActiveRecord::RecordNotSaved) do
793
- composer.save!
794
- end
795
- end
796
- end
797
-
798
- context 'with invalid bookmark and search query' do
799
- let(:params) do
800
- { :job_category => trying_job_template_1.job_category,
801
- :job_template_id => trying_job_template_1.id,
802
- :targeting_type => 'static_query',
803
- :search_query => 'some hosts',
804
- :bookmark_id => bookmark.id,
805
- :inputs => {input1.name => 'some_value'}}
806
- end
807
-
808
- it 'handles errors' do
809
- assert_raises(Foreman::Exception) do
810
- JobInvocationComposer.from_api_params(params)
811
- end
812
- end
813
- end
814
-
815
- context 'with invalid inputs' do
816
- let(:params) do
817
- { :job_category => trying_job_template_1.job_category,
818
- :job_template_id => trying_job_template_1.id,
819
- :targeting_type => 'static_query',
820
- :search_query => 'some hosts',
821
- :inputs => {input3.name => nil}}
822
- end
823
-
824
- it 'handles errors' do
825
- error = assert_raises(ActiveRecord::RecordNotSaved) do
826
- composer.save!
827
- end
828
- assert_includes error.message, "Template #{trying_job_template_1.name}: Input #{input3.name.downcase}: Value can't be blank"
829
- end
830
- end
831
-
832
- context 'with empty values for non-required inputs' do
833
- let(:params) do
834
- { :job_category => trying_job_template_1.job_category,
835
- :job_template_id => trying_job_template_1.id,
836
- :targeting_type => 'static_query',
837
- :search_query => 'some hosts',
838
- :inputs => {input3.name => 'some value'}}
839
- end
840
-
841
- it 'accepts the params' do
842
- composer.save!
843
- assert_not composer.job_invocation.new_record?
844
- end
845
- end
846
-
847
- context 'with missing required inputs' do
848
- let(:params) do
849
- { :job_category => trying_job_template_1.job_category,
850
- :job_template_id => trying_job_template_1.id,
851
- :targeting_type => 'static_query',
852
- :search_query => 'some hosts',
853
- :inputs => {input1.name => 'some_value'}}
854
- end
855
-
856
- it 'handles errors' do
857
- assert_predicate input3, :required
858
-
859
- error = assert_raises(ActiveRecord::RecordNotSaved) do
860
- composer.save!
861
- end
862
-
863
- assert_includes error.message, "Template #{trying_job_template_1.name}: Not all required inputs have values. Missing inputs: #{input3.name}"
864
- end
865
- end
866
- end
867
-
868
- describe '#from_job_invocation' do
869
- let(:job_invocation) do
870
- as_admin { FactoryBot.create(:job_invocation, :with_template, :with_task) }
871
- end
872
-
873
- before do
874
- job_invocation.targeting.host_ids = job_invocation.template_invocations_host_ids
875
- end
876
-
877
- it 'marks targeting as resolved if static' do
878
- created = JobInvocationComposer.from_job_invocation(job_invocation).job_invocation
879
- assert created.targeting.resolved?
880
- created.targeting.save
881
- created.targeting.reload
882
- assert_equal job_invocation.template_invocations_host_ids, created.targeting.targeting_hosts.pluck(:host_id)
883
- end
884
-
885
- it 'takes randomized_ordering from the original job invocation when rerunning failed' do
886
- job_invocation.targeting.randomized_ordering = true
887
- job_invocation.targeting.save!
888
- host_ids = job_invocation.targeting.hosts.pluck(:id)
889
- composer = JobInvocationComposer.from_job_invocation(job_invocation, :host_ids => host_ids)
890
- assert composer.job_invocation.targeting.randomized_ordering
891
- end
892
-
893
- it 'works with invalid hosts' do
894
- host = job_invocation.targeting.hosts.first
895
- ::Host::Managed.any_instance.stubs(:valid?).returns(false)
896
- composer = JobInvocationComposer.from_job_invocation(job_invocation, {})
897
- targeting = composer.compose.job_invocation.targeting
898
- targeting.save!
899
- targeting.reload
900
- assert targeting.valid?
901
- assert_equal targeting.hosts.pluck(:id), [host.id]
902
- end
903
- end
904
-
905
- describe '.for_feature' do
906
- let(:feature) { FactoryBot.create(:remote_execution_feature, job_template: trying_job_template_1) }
907
- let(:host) { FactoryBot.create(:host) }
908
- let(:bookmark) { Bookmark.create!(:query => 'b', :name => 'bookmark', :public => true, :controller => 'hosts') }
909
-
910
- context 'specifying hosts' do
911
- it 'takes a bookmarked search' do
912
- composer = JobInvocationComposer.for_feature(feature.label, bookmark, {})
913
- assert_equal bookmark.id, composer.params['targeting']['bookmark_id']
914
- end
915
-
916
- it 'takes an array of host ids' do
917
- composer = JobInvocationComposer.for_feature(feature.label, [host.id], {})
918
- assert_match(/#{host.name}/, composer.params['targeting']['search_query'])
919
- end
920
-
921
- it 'takes a single host object' do
922
- composer = JobInvocationComposer.for_feature(feature.label, host, {})
923
- assert_match(/#{host.name}/, composer.params['targeting']['search_query'])
924
- end
925
-
926
- it 'takes an array of host FQDNs' do
927
- composer = JobInvocationComposer.for_feature(feature.label, [host.fqdn], {})
928
- assert_match(/#{host.name}/, composer.params['targeting']['search_query'])
929
- end
930
-
931
- it 'takes a search query string' do
932
- composer = JobInvocationComposer.for_feature(feature.label, 'host.example.com', {})
933
- assert_equal 'host.example.com', composer.search_query
934
- end
935
- end
936
- end
937
-
938
- describe '#resolve_job_category and #resolve job_templates' do
939
- let(:setting_template) { as_admin { FactoryBot.create(:job_template, :name => 'trying setting', :job_category => 'fluff') } }
940
- let(:other_template) { as_admin { FactoryBot.create(:job_template, :name => 'trying something', :job_category => 'fluff') } }
941
- let(:second_template) { as_admin { FactoryBot.create(:job_template, :name => 'second template', :job_category => 'fluff') } }
942
- let(:params) { { :host_ids => nil, :targeting => { :targeting_type => "static_query", :bookmark_id => nil }, :job_template_id => setting_template.id } }
943
- let(:composer) { JobInvocationComposer.from_api_params(params) }
944
-
945
- context 'with template in setting present' do
946
- before do
947
- Setting[:remote_execution_form_job_template] = setting_template.name
948
- end
949
-
950
- it 'should resolve category to the setting value' do
951
- assert_equal setting_template.job_category, composer.resolve_job_category('foo')
952
- end
953
-
954
- it 'should resolve template to the setting value' do
955
- assert_equal setting_template, composer.resolve_job_template([other_template, setting_template])
956
- end
957
-
958
- it 'should respect provider templates when resolving templates' do
959
- assert_equal other_template, composer.resolve_job_template([other_template])
960
- end
961
- end
962
-
963
- context 'with template in setting absent' do
964
- it 'should resolve category to the default value' do
965
- category = 'foo'
966
- assert_equal category, composer.resolve_job_category(category)
967
- end
968
-
969
- it 'should resolve template to the first in category' do
970
- assert_equal other_template, composer.resolve_job_template([other_template, second_template])
971
- end
972
- end
973
- end
974
- end