foreman-tasks 0.15.0 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
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));