foreman-tasks 3.0.4 → 4.1.1

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