foreman-tasks 0.15.0 → 0.15.1

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.babelrc +24 -0
  3. data/.eslintrc +27 -0
  4. data/.gitignore +3 -0
  5. data/.prettierrc +4 -0
  6. data/.rubocop.yml +1 -1
  7. data/.storybook/addons.js +2 -0
  8. data/.storybook/config.js +7 -0
  9. data/.storybook/webpack.config.js +66 -0
  10. data/.stylelintrc +5 -0
  11. data/.travis.yml +5 -0
  12. data/.yo-rc.json +5 -0
  13. data/README.md +1 -0
  14. data/app/controllers/foreman_tasks/api/tasks_controller.rb +28 -10
  15. data/app/controllers/foreman_tasks/react_controller.rb +17 -0
  16. data/app/lib/actions/middleware/proxy_batch_triggering.rb +36 -0
  17. data/app/lib/actions/middleware/watch_delegated_proxy_sub_tasks.rb +12 -7
  18. data/app/lib/actions/proxy_action.rb +52 -16
  19. data/app/lib/proxy_api/foreman_dynflow/dynflow_proxy.rb +12 -0
  20. data/app/models/foreman_tasks/remote_task.rb +74 -0
  21. data/app/models/foreman_tasks/task/dynflow_task.rb +14 -7
  22. data/app/models/setting/foreman_tasks.rb +3 -1
  23. data/app/views/foreman_tasks/layouts/react.html.erb +12 -0
  24. data/app/views/foreman_tasks/tasks/show.html.erb +1 -1
  25. data/config/routes.rb +3 -0
  26. data/db/migrate/20181019135324_add_remote_task_operation.rb +5 -0
  27. data/foreman-tasks.gemspec +1 -1
  28. data/lib/foreman_tasks/engine.rb +1 -0
  29. data/lib/foreman_tasks/version.rb +1 -1
  30. data/lib/foreman_tasks.rb +5 -1
  31. data/package.json +117 -0
  32. data/script/travis_run_js_tests.sh +7 -0
  33. data/test/controllers/api/tasks_controller_test.rb +3 -0
  34. data/test/core/unit/dispatcher_test.rb +43 -0
  35. data/test/core/unit/runner_test.rb +129 -0
  36. data/test/core/unit/task_launcher_test.rb +56 -0
  37. data/test/foreman_tasks_core_test_helper.rb +4 -0
  38. data/test/support/dummy_proxy_action.rb +17 -1
  39. data/test/unit/actions/proxy_action_test.rb +20 -2
  40. data/test/unit/actions/recurring_action_test.rb +1 -1
  41. data/test/unit/remote_task_test.rb +41 -0
  42. data/test/unit/task_test.rb +3 -1
  43. data/webpack/ForemanTasks/ForemanTasks.js +27 -0
  44. data/webpack/ForemanTasks/ForemanTasks.test.js +10 -0
  45. data/webpack/ForemanTasks/Routes/ForemanTasksRouter.js +14 -0
  46. data/webpack/ForemanTasks/Routes/ForemanTasksRouter.test.js +22 -0
  47. data/webpack/ForemanTasks/Routes/ForemanTasksRoutes.js +17 -0
  48. data/webpack/ForemanTasks/Routes/ForemanTasksRoutes.test.js +16 -0
  49. data/webpack/ForemanTasks/Routes/IndexTasks/IndexTasks.js +10 -0
  50. data/webpack/ForemanTasks/Routes/IndexTasks/__tests__/IndexTasks.test.js +10 -0
  51. data/webpack/ForemanTasks/Routes/IndexTasks/__tests__/__snapshots__/IndexTasks.test.js.snap +12 -0
  52. data/webpack/ForemanTasks/Routes/IndexTasks/index.js +1 -0
  53. data/webpack/ForemanTasks/Routes/IndexTasks/indexTasks.scss +0 -0
  54. data/webpack/ForemanTasks/Routes/ShowTask/ShowTask.js +10 -0
  55. data/webpack/ForemanTasks/Routes/ShowTask/__tests__/ShowTask.test.js +10 -0
  56. data/webpack/ForemanTasks/Routes/ShowTask/__tests__/__snapshots__/ShowTask.test.js.snap +12 -0
  57. data/webpack/ForemanTasks/Routes/ShowTask/index.js +1 -0
  58. data/webpack/ForemanTasks/Routes/ShowTask/showTask.scss +0 -0
  59. data/webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRouter.test.js.snap +16 -0
  60. data/webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRoutes.test.js.snap +21 -0
  61. data/webpack/ForemanTasks/__snapshots__/ForemanTasks.test.js.snap +60 -0
  62. data/webpack/ForemanTasks/components/Hello/Hello.stories.js +5 -0
  63. data/webpack/ForemanTasks/components/Hello/__tests__/Hello.test.js +11 -0
  64. data/webpack/ForemanTasks/components/Hello/__tests__/__snapshots__/Hello.test.js.snap +7 -0
  65. data/webpack/ForemanTasks/components/Hello/index.js +5 -0
  66. data/webpack/ForemanTasks/index.js +1 -0
  67. data/webpack/index.js +11 -0
  68. data/webpack/stories/index.js +12 -0
  69. data/webpack/test_setup.js +6 -0
  70. metadata +56 -4
data/config/routes.rb CHANGED
@@ -23,6 +23,9 @@ Foreman::Application.routes.draw do
23
23
  end
24
24
  end
25
25
 
26
+ match '/ex_tasks' => 'react#index', :via => [:get]
27
+ match '/ex_tasks/:id' => 'react#index', :via => [:get]
28
+
26
29
  namespace :api do
27
30
  resources :recurring_logics, :only => [:index, :show, :update] do
28
31
  member do
@@ -0,0 +1,5 @@
1
+ class AddRemoteTaskOperation < ActiveRecord::Migration[5.0]
2
+ def change
3
+ add_column :foreman_tasks_remote_tasks, :operation, :string
4
+ end
5
+ end
@@ -29,7 +29,7 @@ same resource. It also optionally provides Dynflow infrastructure for using it f
29
29
  s.extra_rdoc_files = Dir['README*', 'LICENSE']
30
30
 
31
31
  s.add_dependency "foreman-tasks-core"
32
- s.add_dependency "dynflow", '>= 1.2.1'
32
+ s.add_dependency "dynflow", '>= 1.2.2'
33
33
  s.add_dependency "sinatra" # for Dynflow web console
34
34
  s.add_dependency "parse-cron", '~> 0.1.4'
35
35
  s.add_dependency "get_process_mem" # for memory polling
@@ -57,6 +57,7 @@ module ForemanTasks
57
57
 
58
58
  security_block :foreman_tasks do |_map|
59
59
  permission :view_foreman_tasks, { :'foreman_tasks/tasks' => [:auto_complete_search, :sub_tasks, :index, :show],
60
+ :'foreman_tasks/react' => [:index],
60
61
  :'foreman_tasks/api/tasks' => [:bulk_search, :show, :index, :summary] }, :resource_type => ForemanTasks::Task.name
61
62
  permission :edit_foreman_tasks, { :'foreman_tasks/tasks' => [:resume, :unlock, :force_unlock, :cancel_step, :cancel, :abort],
62
63
  :'foreman_tasks/api/tasks' => [:bulk_resume] }, :resource_type => ForemanTasks::Task.name
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = '0.15.0'.freeze
2
+ VERSION = '0.15.1'.freeze
3
3
  end
data/lib/foreman_tasks.rb CHANGED
@@ -12,7 +12,11 @@ module ForemanTasks
12
12
  extend Algebrick::Matching
13
13
 
14
14
  def self.dynflow
15
- @dynflow ||= ForemanTasks::Dynflow.new(nil, ForemanTasks::Dynflow::Configuration.new)
15
+ @dynflow ||= begin
16
+ world = ForemanTasks::Dynflow.new(nil, ForemanTasks::Dynflow::Configuration.new)
17
+ ForemanTasksCore.dynflow_setup(world) if defined?(ForemanTasksCore)
18
+ world
19
+ end
16
20
  end
17
21
 
18
22
  def self.trigger(action, *args, &block)
data/package.json ADDED
@@ -0,0 +1,117 @@
1
+ {
2
+ "name": "foreman-tasks",
3
+ "version": "1.0.0",
4
+ "description": "Foreman Tasks =============",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "lint": "./node_modules/.bin/eslint -c .eslintrc webpack/",
8
+ "test": "node node_modules/.bin/jest --no-cache",
9
+ "test:watch": "node node_modules/.bin/jest --watchAll",
10
+ "test:current": "node node_modules/.bin/jest --watch",
11
+ "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
12
+ "storybook": "start-storybook -p 6006",
13
+ "create-react-component": "yo react-domain"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/theforeman/foreman-tasks.git"
18
+ },
19
+ "bugs": {
20
+ "url": "http://projects.theforeman.org/projects/foreman-tasks/issues"
21
+ },
22
+ "devDependencies": {
23
+ "@storybook/addon-actions": "~3.4.11",
24
+ "@storybook/addon-knobs": "~3.4.11",
25
+ "@storybook/react": "~3.4.11",
26
+ "babel-cli": "^6.10.1",
27
+ "babel-core": "^6.26.3",
28
+ "babel-eslint": "^8.2.3",
29
+ "babel-jest": "^23.6.0",
30
+ "babel-loader": "^7.1.1",
31
+ "babel-plugin-dynamic-import-node": "^2.0.0",
32
+ "babel-plugin-lodash": "^3.3.4",
33
+ "babel-plugin-module-resolver": "^3.2.0",
34
+ "babel-plugin-syntax-dynamic-import": "^6.18.0",
35
+ "babel-plugin-transform-class-properties": "^6.24.1",
36
+ "babel-plugin-transform-object-assign": "^6.8.0",
37
+ "babel-plugin-transform-object-rest-spread": "^6.8.0",
38
+ "babel-preset-env": "^1.7.0",
39
+ "babel-preset-react": "^6.5.0",
40
+ "coveralls": "^3.0.0",
41
+ "enzyme": "^3.4.0",
42
+ "enzyme-adapter-react-16": "^1.4.0",
43
+ "enzyme-to-json": "^3.2.1",
44
+ "eslint": "^4.10.0",
45
+ "eslint-import-resolver-babel-module": "^4.0.0",
46
+ "eslint-plugin-patternfly-react": "0.2.0",
47
+ "jest-cli": "^23.6.0",
48
+ "jest-prop-type-error": "^1.1.0",
49
+ "patternfly": "^3.59.1",
50
+ "prettier": "^1.13.5",
51
+ "raf": "^3.4.0",
52
+ "react-redux-test-utils": "^0.1.1",
53
+ "react-remarkable": "^1.1.3",
54
+ "stylelint": "^9.3.0",
55
+ "stylelint-config-standard": "^18.0.0"
56
+ },
57
+ "dependencies": {
58
+ "babel-polyfill": "^6.26.0",
59
+ "classnames": "^2.2.5",
60
+ "patternfly-react": "^2.29.0",
61
+ "prop-types": "^15.6.0",
62
+ "react": "^16.8.1",
63
+ "react-dom": "^16.8.1",
64
+ "react-redux": "^5.0.6",
65
+ "react-router": "^4.3.1",
66
+ "react-router-bootstrap": "^0.24.4",
67
+ "react-router-dom": "^4.3.1",
68
+ "redux": "^3.6.0",
69
+ "redux-thunk": "^2.3.0",
70
+ "reselect": "^3.0.1",
71
+ "seamless-immutable": "^7.1.2",
72
+ "uuid": "^3.3.2"
73
+ },
74
+ "jest": {
75
+ "automock": true,
76
+ "verbose": true,
77
+ "testMatch": [
78
+ "**/*.test.js"
79
+ ],
80
+ "testURL": "http://localhost/",
81
+ "collectCoverage": true,
82
+ "collectCoverageFrom": [
83
+ "webpack/**/*.js",
84
+ "!webpack/index.js",
85
+ "!webpack/test_setup.js",
86
+ "!webpack/**/bundle*",
87
+ "!webpack/stories/**",
88
+ "!webpack/**/*stories.js"
89
+ ],
90
+ "coverageReporters": [
91
+ "lcov"
92
+ ],
93
+ "unmockedModulePathPatterns": [
94
+ "webpack/",
95
+ "react",
96
+ "node_modules/"
97
+ ],
98
+ "moduleNameMapper": {
99
+ "^.+\\.(png|gif|css|scss)$": "identity-obj-proxy"
100
+ },
101
+ "globals": {
102
+ "__testing__": true
103
+ },
104
+ "transform": {
105
+ "^.+\\.js$": "babel-jest"
106
+ },
107
+ "moduleDirectories": [
108
+ "node_modules",
109
+ "webpack"
110
+ ],
111
+ "setupFiles": [
112
+ "raf/polyfill",
113
+ "jest-prop-type-error",
114
+ "./webpack/test_setup.js"
115
+ ]
116
+ }
117
+ }
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -ev
3
+ if [[ $( git diff --name-only HEAD~1..HEAD webpack/ .travis.yml .babelrc .eslintrc package.json | wc -l ) -ne 0 ]]; then
4
+ npm run test;
5
+ npm run coveralls;
6
+ npm run lint;
7
+ fi
@@ -65,6 +65,9 @@ module ForemanTasks
65
65
  describe 'POST /tasks/callback' do
66
66
  it 'passes the data to the corresponding action' do
67
67
  Support::DummyProxyAction.reset
68
+ ForemanTasks::RemoteTask.any_instance
69
+ .expects(:proxy)
70
+ .returns(Support::DummyProxyAction.proxy)
68
71
 
69
72
  triggered = ForemanTasks.trigger(Support::DummyProxyAction,
70
73
  Support::DummyProxyAction.proxy,
@@ -0,0 +1,43 @@
1
+ require 'foreman_tasks_core_test_helper'
2
+ require 'foreman_tasks/test_helpers'
3
+ require 'foreman_tasks_core/runner'
4
+
5
+ module ForemanTasksCore
6
+ module Runner
7
+ describe Dispatcher::RunnerActor do
8
+ include ForemanTasks::TestHelpers::WithInThreadExecutor
9
+
10
+ let(:dispatcher) { Dispatcher.instance }
11
+ let(:suspended_action) { mock }
12
+ let(:runner) { mock.tap { |r| r.stubs(:id) } }
13
+ let(:clock) { ForemanTasks.dynflow.world.clock }
14
+ let(:logger) { mock.tap { |l| l.stubs(:debug) } }
15
+ let(:actor) do
16
+ Dispatcher::RunnerActor.new dispatcher, suspended_action, runner, clock, logger
17
+ end
18
+
19
+ it 'delivers all updates to actions' do
20
+ targets = (0..2).map { mock }.each_with_index { |mock, index| mock.expects(:<<).with(index) }
21
+ updates = targets.each_with_index.reduce({}) { |acc, (cur, index)| acc.merge(cur => index) }
22
+ runner.expects(:run_refresh).returns(updates)
23
+ actor.expects(:plan_next_refresh)
24
+ actor.refresh_runner
25
+ end
26
+
27
+ it 'plans next refresh' do
28
+ runner.expects(:run_refresh).returns({})
29
+ actor.expects(:plan_next_refresh)
30
+ actor.refresh_runner
31
+ end
32
+
33
+ it 'does not plan next resfresh if done' do
34
+ update = Update.new(nil, 0)
35
+ suspended_action.expects(:<<).with(update)
36
+ runner.expects(:run_refresh).returns(suspended_action => update)
37
+ dispatcher.expects(:finish)
38
+ dispatcher.ticker.expects(:tell).never
39
+ actor.refresh_runner
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,129 @@
1
+ require 'foreman_tasks_core_test_helper'
2
+ require 'foreman_tasks/test_helpers'
3
+ require 'foreman_tasks_core/runner'
4
+ require 'ostruct'
5
+
6
+ module ForemanTasksCore
7
+ module Runner
8
+ class RunnerTest < ActiveSupport::TestCase
9
+ include ForemanTasks::TestHelpers::WithInThreadExecutor
10
+
11
+ describe Base do
12
+ let(:suspended_action) { Class.new }
13
+ let(:runner) { Base.new suspended_action: suspended_action }
14
+
15
+ describe '#generate_updates' do
16
+ it 'returns empty hash when there are no outputs' do
17
+ runner.generate_updates.must_be :empty?
18
+ end
19
+
20
+ it 'returns a hash with outputs' do
21
+ message = 'a message'
22
+ type = 'stdout'
23
+ runner.publish_data(message, type)
24
+ updates = runner.generate_updates
25
+ updates.keys.must_equal [suspended_action]
26
+ update = updates.values.first
27
+ update.exit_status.must_be :nil?
28
+ update.continuous_output.raw_outputs.count.must_equal 1
29
+ end
30
+
31
+ it 'works in compatibility mode' do
32
+ runner = Base.new
33
+ message = 'a message'
34
+ type = 'stdout'
35
+ runner.publish_data(message, type)
36
+ updates = runner.generate_updates
37
+ updates.keys.must_equal [nil]
38
+ update = updates.values.first
39
+ update.exit_status.must_be :nil?
40
+ update.continuous_output.raw_outputs.count.must_equal 1
41
+ end
42
+ end
43
+ end
44
+
45
+ describe Parent do
46
+ let(:suspended_action) { ::Dynflow::Action::Suspended.allocate }
47
+ let(:runner) { Parent.new targets, suspended_action: suspended_action }
48
+ let(:targets) do
49
+ { 'foo' => { 'execution_plan_id' => '123', 'run_step_id' => 2 },
50
+ 'bar' => { 'execution_plan_id' => '456', 'run_step_id' => 2 } }
51
+ end
52
+
53
+ describe '#initialize_continuous_outputs' do
54
+ it 'initializes outputs for targets and parent' do
55
+ outputs = runner.initialize_continuous_outputs
56
+ outputs.keys.count.must_equal 3
57
+ outputs.values.each { |output| output.must_be_instance_of ContinuousOutput }
58
+ end
59
+ end
60
+
61
+ describe '#generate_updates' do
62
+ it 'returns only updates for hosts with pending outputs' do
63
+ runner.generate_updates.must_equal({})
64
+ runner.publish_data_for('foo', 'something', 'something')
65
+ updates = runner.generate_updates
66
+ updates.keys.count.must_equal 1
67
+ end
68
+
69
+ it 'works in compatibility mode' do
70
+ runner = Parent.new targets
71
+ runner.generate_updates.must_equal({})
72
+ runner.broadcast_data('something', 'stdout')
73
+ updates = runner.generate_updates
74
+ updates.keys.count.must_equal 3
75
+ # One of the keys is nil in compatibility mode
76
+ updates.keys.compact.count.must_equal 2
77
+ updates.keys.compact.each do |key|
78
+ key.must_be_instance_of ::Dynflow::Action::Suspended
79
+ end
80
+ end
81
+
82
+ it 'works without compatibility mode' do
83
+ runner.broadcast_data('something', 'stdout')
84
+ updates = runner.generate_updates
85
+ updates.keys.count.must_equal 3
86
+ updates.keys.each do |key|
87
+ key.must_be_instance_of ::Dynflow::Action::Suspended
88
+ end
89
+ end
90
+ end
91
+
92
+ describe '#publish_data_for' do
93
+ it 'publishes data for a single host' do
94
+ runner.publish_data_for('foo', 'message', 'stdout')
95
+ runner.generate_updates.keys.count.must_equal 1
96
+ end
97
+ end
98
+
99
+ describe '#broadcast_data' do
100
+ it 'publishes data for all hosts' do
101
+ runner.broadcast_data('message', 'stdout')
102
+ runner.generate_updates.keys.count.must_equal 3
103
+ end
104
+ end
105
+
106
+ describe '#publish_exception' do
107
+ let(:exception) do
108
+ exception = RuntimeError.new
109
+ exception.stubs(:backtrace).returns([])
110
+ exception
111
+ end
112
+
113
+ before { runner.logger.stubs(:error) }
114
+
115
+ it 'broadcasts the exception to all targets' do
116
+ runner.expects(:publish_exit_status).never
117
+ runner.publish_exception('general failure', exception, false)
118
+ runner.generate_updates.keys.count.must_equal 3
119
+ end
120
+
121
+ it 'publishes exit status if fatal' do
122
+ runner.expects(:publish_exit_status)
123
+ runner.publish_exception('general failure', exception, true)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,56 @@
1
+ require 'foreman_tasks_core_test_helper'
2
+ require 'foreman_tasks/test_helpers'
3
+
4
+ module ForemanTasksCore
5
+ module TaskLauncher
6
+ class TaskLauncherTest < ActiveSupport::TestCase
7
+ include ForemanTasks::TestHelpers::WithInThreadExecutor
8
+
9
+ describe ForemanTasksCore::TaskLauncher do
10
+ let(:launcher) { launcher_class.new ForemanTasks.dynflow.world, {} }
11
+ let(:launcher_input) { { 'action_class' => Support::DummyDynflowAction.to_s, 'action_input' => input } }
12
+ let(:input) { { :do => :something } }
13
+ let(:expected_result) { input.merge(:callback_host => {}) }
14
+
15
+ describe ForemanTasksCore::TaskLauncher::Single do
16
+ let(:launcher_class) { Single }
17
+
18
+ it 'triggers an action' do
19
+ Support::DummyDynflowAction.any_instance.expects(:plan).with do |arg|
20
+ arg.must_equal(expected_result)
21
+ end
22
+ launcher.launch!(launcher_input)
23
+ end
24
+
25
+ it 'provides results' do
26
+ plan = launcher.launch!(launcher_input).finished.value!
27
+ launcher.results[:result].must_equal 'success'
28
+ plan.result.must_equal :success
29
+ end
30
+ end
31
+
32
+ describe ForemanTasksCore::TaskLauncher::Batch do
33
+ let(:launcher_class) { Batch }
34
+
35
+ it 'triggers the actions' do
36
+ Support::DummyDynflowAction.any_instance.expects(:plan).with { |arg| arg == expected_result }.twice
37
+ parent = launcher.launch!('foo' => launcher_input, 'bar' => launcher_input)
38
+ plan = parent.finished.value!
39
+ plan.result.must_equal :success
40
+ plan.sub_plans.count.must_equal 2
41
+ end
42
+
43
+ it 'provides results' do
44
+ launcher.launch!('foo' => launcher_input, 'bar' => launcher_input)
45
+ launcher.results.keys.must_equal %w[foo bar]
46
+ launcher.results.values.each do |result|
47
+ plan = ForemanTasks.dynflow.world.persistence.load_execution_plan(result[:task_id])
48
+ result[:result].must_equal 'success'
49
+ plan.result.must_equal :success
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,4 @@
1
+ require 'foreman_tasks_test_helper'
2
+ require 'foreman_tasks_core'
3
+
4
+ ForemanTasksCore.dynflow_setup ForemanTasks.dynflow.world
@@ -2,6 +2,14 @@ require 'securerandom'
2
2
 
3
3
  module Support
4
4
  class DummyProxyAction < Actions::ProxyAction
5
+ class DummyProxyVersion
6
+ attr_reader :version
7
+
8
+ def initialize(version)
9
+ @version = { 'version' => version }
10
+ end
11
+ end
12
+
5
13
  class DummyProxy
6
14
  attr_reader :log, :task_triggered, :uuid
7
15
 
@@ -14,7 +22,7 @@ module Support
14
22
  def trigger_task(*args)
15
23
  @log[:trigger_task] << args
16
24
  @task_triggered.fulfill(true)
17
- { 'task_id' => @uuid }
25
+ { 'task_id' => @uuid, 'result' => 'success' }
18
26
  end
19
27
 
20
28
  def cancel_task(*args)
@@ -24,6 +32,10 @@ module Support
24
32
  def url
25
33
  'proxy.example.com'
26
34
  end
35
+
36
+ def statuses
37
+ { version: DummyProxyVersion.new('1.21.0') }
38
+ end
27
39
  end
28
40
 
29
41
  class ProxySelector < ::ForemanTasks::ProxySelector
@@ -32,6 +44,10 @@ module Support
32
44
  end
33
45
  end
34
46
 
47
+ def proxy_operation_name
48
+ 'support'
49
+ end
50
+
35
51
  def proxy
36
52
  self.class.proxy
37
53
  end
@@ -8,14 +8,18 @@ module ForemanTasks
8
8
  let(:secrets) do
9
9
  { 'logins' => { 'admin' => 'changeme', 'root' => 'toor' } }
10
10
  end
11
+ let(:batch_triggering) { false }
11
12
 
12
13
  before do
14
+ Support::DummyProxyAction.any_instance.stubs(:with_batch_triggering?).returns(batch_triggering)
13
15
  Support::DummyProxyAction.reset
16
+ RemoteTask.any_instance.stubs(:proxy).returns(Support::DummyProxyAction.proxy)
14
17
  @action = create_and_plan_action(Support::DummyProxyAction,
15
18
  Support::DummyProxyAction.proxy,
16
19
  'Proxy::DummyAction',
17
20
  'foo' => 'bar',
18
- 'secrets' => secrets)
21
+ 'secrets' => secrets,
22
+ 'use_batch_triggering' => batch_triggering)
19
23
  @action = run_action(@action)
20
24
  end
21
25
 
@@ -26,12 +30,26 @@ module ForemanTasks
26
30
  { 'foo' => 'bar',
27
31
  'secrets' => secrets,
28
32
  'connection_options' =>
29
- { 'retry_interval' => 15, 'retry_count' => 4 },
33
+ { 'retry_interval' => 15, 'retry_count' => 4,
34
+ 'proxy_batch_triggering' => batch_triggering },
35
+ 'use_batch_triggering' => batch_triggering,
30
36
  'proxy_url' => 'proxy.example.com',
31
37
  'proxy_action_name' => 'Proxy::DummyAction',
38
+ "proxy_version" => { "major" => 1, "minor" => 21, "patch" => 0 },
32
39
  'callback' => { 'task_id' => Support::DummyProxyAction.proxy.uuid, 'step_id' => @action.run_step_id } }]
33
40
  proxy_call.must_equal(expected_call)
34
41
  end
42
+
43
+ describe 'with batch triggering' do
44
+ let(:batch_triggering) { true }
45
+ it 'create remote tasks for batch triggering' do
46
+ task = RemoteTask.first
47
+ task.state.must_equal 'new'
48
+ task.execution_plan_id.must_equal @action.execution_plan_id
49
+ task.operation.must_equal 'support'
50
+ task.remote_task_id.must_be :nil?
51
+ end
52
+ end
35
53
  end
36
54
 
37
55
  describe 'resumed run' do
@@ -55,7 +55,7 @@ module ForemanTasks
55
55
 
56
56
  specify 'it triggers the repeat when the task goes into planned state' do
57
57
  delay_options = recurring_logic.generate_delay_options
58
- task = ForemanTasks.delay HookedAction, delay_options, args
58
+ task = ForemanTasks.delay HookedAction, delay_options, *args
59
59
  recurring_logic.tasks.count.must_equal 1
60
60
 
61
61
  # Perform planning of the delayed plan
@@ -0,0 +1,41 @@
1
+ require 'foreman_tasks_test_helper'
2
+
3
+ module ForemanTasks
4
+ class RemoteTaskTest < ActiveSupport::TestCase
5
+ describe 'batch triggering' do
6
+ let(:remote_tasks) do
7
+ (1..5).map do |i|
8
+ task = RemoteTask.new :execution_plan_id => i, :step_id => 1, :proxy_url => "something"
9
+ task.expects(:proxy_input).returns({})
10
+ task.expects(:proxy_action_name).returns('MyProxyAction')
11
+ task.save!
12
+ task
13
+ end
14
+ end
15
+
16
+ it 'triggers in batches' do
17
+ results = remote_tasks.reduce({}) do |acc, cur|
18
+ acc.merge(cur.execution_plan_id.to_s => { 'task_id' => cur.id + 5, 'result' => 'success' })
19
+ end
20
+
21
+ fake_proxy = mock
22
+ fake_proxy.expects(:launch_tasks).returns(results)
23
+ remote_tasks.first.expects(:proxy).returns(fake_proxy)
24
+ RemoteTask.batch_trigger('a_operation', remote_tasks)
25
+ remote_tasks.each do |remote_task|
26
+ remote_task.reload
27
+ remote_task.state.must_equal 'triggered'
28
+ remote_task.remote_task_id.must_equal((remote_task.id + 5).to_s)
29
+ end
30
+ end
31
+
32
+ it 'fallbacks to old way when batch trigger gets 404' do
33
+ fake_proxy = mock
34
+ fake_proxy.expects(:launch_tasks).raises(RestClient::NotFound.new)
35
+ remote_tasks.first.expects(:proxy).returns(fake_proxy)
36
+ remote_tasks.each { |task| task.expects(:trigger) }
37
+ RemoteTask.batch_trigger('a_operation', remote_tasks)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -84,9 +84,11 @@ class TasksTest < ActiveSupport::TestCase
84
84
  end
85
85
 
86
86
  describe 'task without valid execution plan' do
87
+ let(:missing_task_uuid) { '11111111-2222-3333-4444-555555555555' }
88
+
87
89
  let(:task) do
88
90
  task = FactoryBot.create(:dynflow_task).tap do |task|
89
- task.external_id = 'missing-task'
91
+ task.external_id = missing_task_uuid
90
92
  task.save
91
93
  end
92
94
  ForemanTasks::Task.find(task.id)
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import { BrowserRouter } from 'react-router-dom';
3
+ import { LinkContainer } from 'react-router-bootstrap';
4
+ import { ButtonGroup, Button } from 'patternfly-react';
5
+
6
+ import routes from './Routes/ForemanTasksRoutes';
7
+ import ForemanTasksRouter from './Routes/ForemanTasksRouter';
8
+
9
+ const ForemanTasks = () => (
10
+ <BrowserRouter>
11
+ <div>
12
+ <div style={{ paddingTop: '10px', paddingBottom: '10px' }}>
13
+ <ButtonGroup bsSize="large">
14
+ <LinkContainer to={routes.indexTasks.path}>
15
+ <Button bsStyle="link">index-tasks-page</Button>
16
+ </LinkContainer>
17
+ <LinkContainer to={routes.showTask.path.replace(':id', 'some-id')}>
18
+ <Button bsStyle="link">show-task-page</Button>
19
+ </LinkContainer>
20
+ </ButtonGroup>
21
+ </div>
22
+ <ForemanTasksRouter />
23
+ </div>
24
+ </BrowserRouter>
25
+ );
26
+
27
+ export default ForemanTasks;
@@ -0,0 +1,10 @@
1
+ import { testComponentSnapshotsWithFixtures } from 'react-redux-test-utils';
2
+
3
+ import ForemanTasks from './ForemanTasks';
4
+
5
+ const fixtures = {
6
+ 'render without Props': {},
7
+ };
8
+
9
+ describe('ForemanTasks', () =>
10
+ testComponentSnapshotsWithFixtures(ForemanTasks, fixtures));
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { Switch, Route } from 'react-router-dom';
3
+
4
+ import routes from './ForemanTasksRoutes';
5
+
6
+ const ForemanTasksRouter = () => (
7
+ <Switch>
8
+ {Object.entries(routes).map(([key, props]) => (
9
+ <Route key={key} {...props} />
10
+ ))}
11
+ </Switch>
12
+ );
13
+
14
+ export default ForemanTasksRouter;
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { testComponentSnapshotsWithFixtures } from 'react-redux-test-utils';
3
+
4
+ import ForemanTasksRouter from './ForemanTasksRouter';
5
+
6
+ jest.mock('./ForemanTasksRoutes', () => ({
7
+ someRoute: {
8
+ path: '/some-route',
9
+ render: props => <span {...props}>some-route</span>,
10
+ },
11
+ someOtherRoute: {
12
+ path: '/some-other-route',
13
+ render: props => <span {...props}>some-other-route</span>,
14
+ },
15
+ }));
16
+
17
+ const fixtures = {
18
+ 'render without Props': {},
19
+ };
20
+
21
+ describe('ForemanTasksRouter', () =>
22
+ testComponentSnapshotsWithFixtures(ForemanTasksRouter, fixtures));