foreman-tasks 3.0.4 → 4.1.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby_tests.yml +5 -3
  3. data/app/controllers/foreman_tasks/api/tasks_controller.rb +2 -5
  4. data/app/lib/actions/entry_action.rb +8 -4
  5. data/app/lib/actions/helpers/lock.rb +11 -5
  6. data/app/lib/actions/middleware/keep_current_request_id.rb +4 -1
  7. data/app/lib/actions/middleware/keep_current_user.rb +11 -1
  8. data/app/lib/actions/observable_action.rb +80 -0
  9. data/app/lib/actions/proxy_action.rb +2 -4
  10. data/app/models/foreman_tasks/concerns/action_subject.rb +0 -6
  11. data/app/models/foreman_tasks/link.rb +60 -0
  12. data/app/models/foreman_tasks/lock.rb +30 -128
  13. data/app/models/foreman_tasks/task.rb +20 -7
  14. data/app/models/foreman_tasks/task/search.rb +7 -6
  15. data/app/views/foreman_tasks/api/locks/show.json.rabl +4 -0
  16. data/app/views/foreman_tasks/api/tasks/details.json.rabl +5 -3
  17. data/app/views/foreman_tasks/tasks/_lock_card.html.erb +10 -0
  18. data/db/migrate/20180927120509_add_user_id.foreman_tasks.rb +4 -2
  19. data/db/migrate/20181206123910_create_foreman_tasks_links.foreman_tasks.rb +26 -0
  20. data/db/migrate/20181206124952_migrate_non_exclusive_locks_to_links.foreman_tasks.rb +14 -0
  21. data/db/migrate/20181206131436_drop_old_locks.foreman_tasks.rb +20 -0
  22. data/db/migrate/20181206131627_make_locks_exclusive.foreman_tasks.rb +25 -0
  23. data/lib/foreman_tasks/cleaner.rb +10 -0
  24. data/lib/foreman_tasks/engine.rb +5 -2
  25. data/lib/foreman_tasks/tasks/export_tasks.rake +2 -2
  26. data/lib/foreman_tasks/version.rb +1 -1
  27. data/package.json +6 -6
  28. data/test/controllers/api/tasks_controller_test.rb +1 -1
  29. data/test/controllers/tasks_controller_test.rb +3 -3
  30. data/test/core/unit/runner_test.rb +4 -17
  31. data/test/factories/task_factory.rb +31 -4
  32. data/test/unit/actions/action_with_sub_plans_test.rb +5 -2
  33. data/test/unit/actions/proxy_action_test.rb +4 -1
  34. data/test/unit/cleaner_test.rb +4 -4
  35. data/test/unit/locking_test.rb +85 -0
  36. data/test/unit/task_test.rb +15 -13
  37. data/test/unit/triggering_test.rb +2 -2
  38. data/webpack/ForemanTasks/Components/TaskDetails/Components/Locks.js +2 -2
  39. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +3 -2
  40. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/Locks.test.js.snap +4 -4
  41. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/TaskInfo.test.js.snap +2 -0
  42. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +4 -1
  43. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +5 -1
  44. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +3 -0
  45. data/webpack/ForemanTasks/Components/TaskDetails/index.js +2 -0
  46. data/webpack/ForemanTasks/Components/TasksTable/TasksTablePage.scss +4 -3
  47. data/webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTablePage.test.js.snap +2 -2
  48. data/webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionNameCellFormatter.test.js.snap +2 -3
  49. data/webpack/ForemanTasks/Components/TasksTable/formatters/actionNameCellFormatter.js +2 -3
  50. metadata +12 -4
  51. data/test/unit/lock_test.rb +0 -22
@@ -34,8 +34,9 @@ module ForemanTasks
34
34
  describe Actions::ActionWithSubPlans do
35
35
  include ForemanTasks::TestHelpers::WithInThreadExecutor
36
36
 
37
+ let(:user) { FactoryBot.create(:user) }
38
+
37
39
  let(:task) do
38
- user = FactoryBot.create(:user)
39
40
  triggered = ForemanTasks.trigger(ParentAction, user)
40
41
  raise triggered.error if triggered.respond_to?(:error)
41
42
  triggered.finished.wait(30)
@@ -49,7 +50,9 @@ module ForemanTasks
49
50
 
50
51
  specify "the locks of the sub-plan don't colide with the locks of its parent" do
51
52
  child_task = task.sub_tasks.first
52
- assert(child_task.locks.any? { |lock| lock.name == 'write' }, "it's locks don't conflict with parent's")
53
+ assert_not(child_task.locks.any?, "the lock is ensured by the parent")
54
+ found = ForemanTasks::Link.for_resource(user).where(:task_id => child_task.id).any?
55
+ assert(found, "the action is linked properly")
53
56
  end
54
57
  end
55
58
  end
@@ -14,6 +14,9 @@ module ForemanTasks
14
14
  Support::DummyProxyAction.any_instance.stubs(:with_batch_triggering?).returns(batch_triggering)
15
15
  Support::DummyProxyAction.reset
16
16
  RemoteTask.any_instance.stubs(:proxy).returns(Support::DummyProxyAction.proxy)
17
+ Setting.stubs(:[]).with('foreman_tasks_proxy_action_retry_interval')
18
+ Setting.stubs(:[]).with('foreman_tasks_proxy_action_retry_count')
19
+ Setting.stubs(:[]).with('foreman_tasks_proxy_batch_trigger')
17
20
  @action = create_and_plan_action(Support::DummyProxyAction,
18
21
  Support::DummyProxyAction.proxy,
19
22
  'Proxy::DummyAction',
@@ -107,7 +110,7 @@ module ForemanTasks
107
110
  _(action.world.clock.pending_pings.length).must_equal 1
108
111
  _(action.output[:metadata][:failed_proxy_tasks].length).must_equal 1
109
112
  2.times { action.output[:metadata][:failed_proxy_tasks] << {} }
110
- _ { proc { action = run_stubbed_action.call action } }.must_raise(Errno::ECONNREFUSED)
113
+ _(proc { action = run_stubbed_action.call action }).must_raise(Errno::ECONNREFUSED)
111
114
  _(action.state).must_equal :error
112
115
  end
113
116
 
@@ -60,18 +60,18 @@ class TasksTest < ActiveSupport::TestCase
60
60
  task.started_at = task.ended_at = Time.zone.now
61
61
  task.save
62
62
  end]
63
- lock_to_delete = tasks_to_delete.first.locks.create(:name => 'read', :resource => User.current)
63
+ link_to_delete = tasks_to_delete.first.links.create(:resource => User.current)
64
64
 
65
65
  tasks_to_keep = [FactoryBot.create(:dynflow_task, :product_create_task)]
66
- lock_to_keep = tasks_to_keep.first.locks.create(:name => 'read', :resource => User.current)
66
+ link_to_keep = tasks_to_keep.first.links.create(:resource => User.current)
67
67
 
68
68
  cleaner.expects(:tasks_to_csv)
69
69
  cleaner.delete
70
70
  _(ForemanTasks::Task.where(id: tasks_to_delete)).must_be_empty
71
71
  _(ForemanTasks::Task.where(id: tasks_to_keep)).must_equal tasks_to_keep
72
72
 
73
- _(ForemanTasks::Lock.find_by(id: lock_to_delete.id)).must_be_nil
74
- _(ForemanTasks::Lock.find_by(id: lock_to_keep.id)).wont_be_nil
73
+ _(ForemanTasks::Link.find_by(id: link_to_delete.id)).must_be_nil
74
+ _(ForemanTasks::Link.find_by(id: link_to_keep.id)).wont_be_nil
75
75
  end
76
76
 
77
77
  it 'supports passing empty filter (just delete all)' do
@@ -0,0 +1,85 @@
1
+ require 'ostruct'
2
+ require 'foreman_tasks_test_helper'
3
+
4
+ module ForemanTasks
5
+ class LockTest < ::ActiveSupport::TestCase
6
+ describe ::ForemanTasks::Lock::LockConflict do
7
+ class FakeLockConflict < ForemanTasks::Lock::LockConflict
8
+ def _(val)
9
+ val.freeze
10
+ end
11
+ end
12
+
13
+ it 'does not modify frozen strings' do
14
+ required_lock = OpenStruct.new(:name => 'my_lock')
15
+ # Before #21770 the next line would raise
16
+ # RuntimeError: can't modify frozen String
17
+ conflict = FakeLockConflict.new(required_lock, [])
18
+ assert conflict._('this should be frozen').frozen?
19
+ end
20
+ end
21
+
22
+ describe 'locking and linking' do
23
+ before { [Lock, Link].each(&:destroy_all) }
24
+ let(:task1) { FactoryBot.create(:some_task) }
25
+ let(:task2) { FactoryBot.create(:some_task) }
26
+ let(:resource) { FactoryBot.create(:user) }
27
+
28
+ describe Lock do
29
+ it 'can lock a resource for a single task' do
30
+ Lock.lock!(resource, task1)
31
+ end
32
+
33
+ it 'can lock a resource for a single task only once' do
34
+ Lock.lock!(resource, task1)
35
+ _(Lock.for_resource(resource).count).must_equal 1
36
+ Lock.lock!(resource, task1)
37
+ _(Lock.for_resource(resource).count).must_equal 1
38
+ end
39
+
40
+ it 'cannot lock a resource for multiple tasks' do
41
+ lock = Lock.lock!(resource, task1)
42
+ _(Lock.colliding_locks(resource, task2)).must_equal [lock]
43
+ assert_raises Lock::LockConflict do
44
+ Lock.lock!(resource, task2)
45
+ end
46
+ end
47
+
48
+ it 'raises LockConflict when enforced by db' do
49
+ lock = Lock.lock!(resource, task1)
50
+ Lock.any_instance
51
+ .expects(:colliding_locks).twice.returns([], [lock])
52
+ exception = assert_raises Lock::LockConflict do
53
+ Lock.lock!(resource, task2)
54
+ end
55
+ _(exception.message).must_match(/#{lock.task_id}/)
56
+ end
57
+
58
+ it 'creates a link when creating a lock for a resource' do
59
+ Lock.lock!(resource, task1)
60
+ link = Link.for_resource(resource).first
61
+ _(link.task_id).must_equal task1.id
62
+ end
63
+ end
64
+
65
+ describe Link do
66
+ it 'can link a resource to a single task' do
67
+ Link.link!(resource, task1)
68
+ end
69
+
70
+ it 'can link a resource for a single task only once' do
71
+ Link.link!(resource, task1)
72
+ _(Link.for_resource(resource).count).must_equal 1
73
+ Link.link!(resource, task1)
74
+ _(Link.for_resource(resource).count).must_equal 1
75
+ end
76
+
77
+ it 'can link a resource to multiple tasks' do
78
+ Link.link!(resource, task1)
79
+ Link.link!(resource, task2)
80
+ _(Link.for_resource(resource).count).must_equal 2
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -31,9 +31,9 @@ class TasksTest < ActiveSupport::TestCase
31
31
  assert_equal [@task_one], ForemanTasks::Task.search_for("user = #{@user_one.login}")
32
32
  end
33
33
 
34
- test 'cannot search by arbitrary key' do
35
- _ { proc { ForemanTasks::Task.search_for('user.my_key ~ 5') } }.must_raise(ScopedSearch::QueryNotSupported)
36
- _ { proc { ForemanTasks::Task.search_for('user. = 5') } }.must_raise(ScopedSearch::QueryNotSupported)
34
+ it 'cannot search by arbitrary key' do
35
+ _(proc { ForemanTasks::Task.search_for('user.my_key ~ 5') }).must_raise(ScopedSearch::QueryNotSupported)
36
+ _(proc { ForemanTasks::Task.search_for('user. = 5') }).must_raise(ScopedSearch::QueryNotSupported)
37
37
  end
38
38
 
39
39
  test 'can search the tasks by negated user' do
@@ -59,8 +59,8 @@ class TasksTest < ActiveSupport::TestCase
59
59
  end
60
60
 
61
61
  test 'cannot glob on user\'s id' do
62
- _ { proc { ForemanTasks::Task.search_for("user.id ~ something") } }.must_raise(ScopedSearch::QueryNotSupported)
63
- _ { proc { ForemanTasks::Task.search_for("user.id ~ 5") } }.must_raise(ScopedSearch::QueryNotSupported)
62
+ _(proc { ForemanTasks::Task.search_for("user.id ~ something") }).must_raise(ScopedSearch::QueryNotSupported)
63
+ _(proc { ForemanTasks::Task.search_for("user.id ~ 5") }).must_raise(ScopedSearch::QueryNotSupported)
64
64
  end
65
65
 
66
66
  test 'can search the tasks by user with wildcards' do
@@ -126,15 +126,17 @@ class TasksTest < ActiveSupport::TestCase
126
126
  end
127
127
 
128
128
  it 'raises an exception if duration is unknown' do
129
- _ { proc { ForemanTasks::Task.search_for('duration = "25 potatoes"') } }.must_raise ScopedSearch::QueryNotSupported
129
+ _(proc { ForemanTasks::Task.search_for('duration = "25 potatoes"') }).must_raise ScopedSearch::QueryNotSupported
130
130
  end
131
131
  end
132
132
 
133
133
  context 'by taxonomies' do
134
134
  test 'can search by taxonomies using IN' do
135
- assert_nothing_raised(PG::SyntaxError) { ForemanTasks::Task.search_for('location_id ^ (1)').first }
136
- assert_nothing_raised(PG::SyntaxError) { ForemanTasks::Task.search_for('organization_id ^ (1)').first }
137
- assert_nothing_raised(PG::SyntaxError) { ForemanTasks::Task.search_for('organization_id = 1').first }
135
+ assert_nothing_raised { ForemanTasks::Task.search_for('location_id ^ (1)').first }
136
+ assert_nothing_raised { ForemanTasks::Task.search_for('organization_id ^ (1)').first }
137
+ assert_nothing_raised { ForemanTasks::Task.search_for('location_id ^ (1,2)').first }
138
+ assert_nothing_raised { ForemanTasks::Task.search_for('organization_id ^ (1,2)').first }
139
+ assert_nothing_raised { ForemanTasks::Task.search_for('organization_id = 1').first }
138
140
  end
139
141
  end
140
142
  end
@@ -300,7 +302,7 @@ class TasksTest < ActiveSupport::TestCase
300
302
  resource_type = 'restype1'
301
303
 
302
304
  task1_old = FactoryBot.create(
303
- :task_with_locks,
305
+ :task_with_links,
304
306
  started_at: '2019-10-01 11:15:55',
305
307
  ended_at: '2019-10-01 11:15:57',
306
308
  resource_id: 1,
@@ -308,7 +310,7 @@ class TasksTest < ActiveSupport::TestCase
308
310
  resource_type: resource_type
309
311
  )
310
312
  task1_new = FactoryBot.create(
311
- :task_with_locks,
313
+ :task_with_links,
312
314
  started_at: '2019-10-02 11:15:55',
313
315
  ended_at: '2019-10-02 11:15:57',
314
316
  resource_id: 1,
@@ -316,7 +318,7 @@ class TasksTest < ActiveSupport::TestCase
316
318
  resource_type: resource_type
317
319
  )
318
320
  task2 = FactoryBot.create(
319
- :task_with_locks,
321
+ :task_with_links,
320
322
  started_at: '2019-10-03 11:15:55',
321
323
  ended_at: '2019-10-03 11:15:57',
322
324
  resource_id: 2,
@@ -324,7 +326,7 @@ class TasksTest < ActiveSupport::TestCase
324
326
  resource_type: resource_type
325
327
  )
326
328
  task3 = FactoryBot.create(
327
- :task_with_locks,
329
+ :task_with_links,
328
330
  started_at: '2019-10-03 11:15:55',
329
331
  ended_at: '2019-10-03 11:15:57',
330
332
  resource_id: 3,
@@ -24,7 +24,7 @@ class TriggeringTest < ActiveSupport::TestCase
24
24
  it 'cannot have mode set to arbitrary value' do
25
25
  triggering = FactoryBot.build(:triggering)
26
26
  _(triggering).must_be :valid?
27
- _ { proc { triggering.mode = 'bogus' } }.must_raise ArgumentError
28
- _ { proc { triggering.mode = 27 } }.must_raise ArgumentError
27
+ _(proc { triggering.mode = 'bogus' }).must_raise ArgumentError
28
+ _(proc { triggering.mode = 27 }).must_raise ArgumentError
29
29
  end
30
30
  end
@@ -21,10 +21,10 @@ const Locks = ({ locks }) => (
21
21
  lock.exclusive ? 'fa-lock' : 'fa-unlock-alt'
22
22
  }`}
23
23
  />
24
- {lock.name}
24
+ {lock.resource_type}
25
25
  </Card.Title>
26
26
  <Card.Body>
27
- {`${lock.resource_type} id:${lock.resource_id}`}
27
+ {`id:${lock.resource_id}`}
28
28
  <br />
29
29
  </Card.Body>
30
30
  </Card>
@@ -69,6 +69,7 @@ class TaskInfo extends Component {
69
69
  {
70
70
  title: 'Name',
71
71
  value: action || __('N/A'),
72
+ className: 'details-name',
72
73
  },
73
74
  {
74
75
  title: 'Start at',
@@ -129,7 +130,7 @@ class TaskInfo extends Component {
129
130
  {__(items[0].title)}:
130
131
  </span>
131
132
  </Col>
132
- <Col md={5} sm={6}>
133
+ <Col md={5} sm={6} className={items[0].className}>
133
134
  <span>{items[0].value}</span>
134
135
  </Col>
135
136
  <Col md={2} sm={6}>
@@ -137,7 +138,7 @@ class TaskInfo extends Component {
137
138
  {__(items[1].title)}:
138
139
  </span>
139
140
  </Col>
140
- <Col md={3} sm={6}>
141
+ <Col md={3} sm={6} className={items[1].className}>
141
142
  {items[1].value}
142
143
  </Col>
143
144
  </Row>
@@ -39,12 +39,12 @@ exports[`Locks rendering render with Props 1`] = `
39
39
  <span
40
40
  className="fa fa-unlock-alt"
41
41
  />
42
- task_owner
42
+ User
43
43
  </CardTitle>
44
44
  <CardBody
45
45
  className=""
46
46
  >
47
- User id:4
47
+ id:4
48
48
  <br />
49
49
  </CardBody>
50
50
  </Card>
@@ -71,12 +71,12 @@ exports[`Locks rendering render with Props 1`] = `
71
71
  <span
72
72
  className="fa fa-unlock-alt"
73
73
  />
74
- task_owner2
74
+ User
75
75
  </CardTitle>
76
76
  <CardBody
77
77
  className=""
78
78
  >
79
- User id:2
79
+ id:2
80
80
  <br />
81
81
  </CardBody>
82
82
  </Card>
@@ -27,6 +27,7 @@ exports[`TaskInfo rendering render with Props 1`] = `
27
27
  </Col>
28
28
  <Col
29
29
  bsClass="col"
30
+ className="details-name"
30
31
  componentClass="div"
31
32
  md={5}
32
33
  sm={6}
@@ -330,6 +331,7 @@ exports[`TaskInfo rendering render without Props 1`] = `
330
331
  </Col>
331
332
  <Col
332
333
  bsClass="col"
334
+ className="details-name"
333
335
  componentClass="div"
334
336
  md={5}
335
337
  sm={6}
@@ -19,6 +19,7 @@ const TaskDetails = ({
19
19
  failedSteps,
20
20
  runningSteps,
21
21
  locks,
22
+ links,
22
23
  cancelStep,
23
24
  taskReloadStart,
24
25
  taskReloadStop,
@@ -87,7 +88,7 @@ const TaskDetails = ({
87
88
  <Errors executionPlan={executionPlan} failedSteps={failedSteps} />
88
89
  </Tab>
89
90
  <Tab eventKey={4} disabled={isLoading} title={__('Locks')}>
90
- <Locks locks={locks} />
91
+ <Locks locks={locks.concat(links)} />
91
92
  </Tab>
92
93
  <Tab eventKey={5} disabled={isLoading} title={__('Raw')}>
93
94
  <Raw
@@ -114,6 +115,7 @@ TaskDetails.propTypes = {
114
115
  APIerror: PropTypes.object,
115
116
  taskReloadStop: PropTypes.func.isRequired,
116
117
  taskReloadStart: PropTypes.func.isRequired,
118
+ links: PropTypes.array,
117
119
  ...Task.propTypes,
118
120
  ...Errors.propTypes,
119
121
  ...Locks.propTypes,
@@ -124,6 +126,7 @@ TaskDetails.defaultProps = {
124
126
  runningSteps: [],
125
127
  APIerror: null,
126
128
  status: STATUS.PENDING,
129
+ links: [],
127
130
  ...Task.defaultProps,
128
131
  ...RunningSteps.defaultProps,
129
132
  ...Errors.defaultProps,
@@ -51,7 +51,7 @@
51
51
  }
52
52
 
53
53
  pre {
54
- white-space: pre;
54
+ white-space: pre-wrap;
55
55
  word-break: normal;
56
56
  }
57
57
 
@@ -59,4 +59,8 @@
59
59
  display: inline-block;
60
60
  width: 10em;
61
61
  }
62
+
63
+ .details-name {
64
+ overflow-wrap: anywhere;
65
+ }
62
66
  }
@@ -70,6 +70,9 @@ export const selectHasSubTasks = state =>
70
70
  export const selectLocks = state =>
71
71
  selectTaskDetailsResponse(state).locks || [];
72
72
 
73
+ export const selectLinks = state =>
74
+ selectTaskDetailsResponse(state).links || [];
75
+
73
76
  export const selectUsernamePath = state =>
74
77
  selectTaskDetailsResponse(state)?.username_path;
75
78
 
@@ -9,6 +9,7 @@ import {
9
9
  selectStartBefore,
10
10
  selectStartedAt,
11
11
  selectLocks,
12
+ selectLinks,
12
13
  selectInput,
13
14
  selectOutput,
14
15
  selectResumable,
@@ -56,6 +57,7 @@ const mapStateToProps = state => ({
56
57
  help: selectHelp(state),
57
58
  hasSubTasks: selectHasSubTasks(state),
58
59
  locks: selectLocks(state),
60
+ links: selectLinks(state),
59
61
  usernamePath: selectUsernamePath(state),
60
62
  action: selectAction(state),
61
63
  state: selectState(state),
@@ -1,8 +1,9 @@
1
- .tasks-pagination {
2
- margin-top: -6px;
3
- }
4
1
  .tasks-table {
5
2
  margin-bottom: 70px;
3
+
4
+ .action-name-tasks-table {
5
+ overflow-wrap: anywhere;
6
+ }
6
7
  }
7
8
 
8
9
  .tasks-table-wrapper {
@@ -17,7 +17,7 @@ exports[`TasksTablePage rendering render with Breadcrubs and edit permissions 1`
17
17
  />
18
18
  <PageLayout
19
19
  beforeToolbarComponent={
20
- <UNDEFINED
20
+ <Memo(Connect(TasksDashboard))
21
21
  history={
22
22
  Object {
23
23
  "location": Object {
@@ -169,7 +169,7 @@ exports[`TasksTablePage rendering render with minimal props 1`] = `
169
169
  />
170
170
  <PageLayout
171
171
  beforeToolbarComponent={
172
- <UNDEFINED
172
+ <Memo(Connect(TasksDashboard))
173
173
  history={
174
174
  Object {
175
175
  "location": Object {
@@ -2,10 +2,9 @@
2
2
 
3
3
  exports[`actionNameCellFormatter render 1`] = `
4
4
  <a
5
+ className="action-name-tasks-table"
5
6
  href="/some-url/some-id"
6
7
  >
7
- <EllipisWithTooltip>
8
- action-name
9
- </EllipisWithTooltip>
8
+ action-name
10
9
  </a>
11
10
  `;