moose-inventory 1.0.9 → 2.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -1
  3. data/.github/workflows/release.yml +58 -0
  4. data/.gitleaks.toml +9 -0
  5. data/.rubocop.yml +28 -0
  6. data/BACKLOG.md +130 -24
  7. data/Gemfile.lock +36 -1
  8. data/README.md +26 -6
  9. data/Rakefile +1 -1
  10. data/docs/release/publishing.md +44 -48
  11. data/docs/release/release-readiness.md +14 -0
  12. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  13. data/docs/security-audit-2026-05-26.md +63 -0
  14. data/lib/moose_inventory/cli/group.rb +3 -0
  15. data/lib/moose_inventory/cli/group_add.rb +89 -73
  16. data/lib/moose_inventory/cli/group_addchild.rb +77 -60
  17. data/lib/moose_inventory/cli/group_addhost.rb +78 -65
  18. data/lib/moose_inventory/cli/group_rm.rb +101 -71
  19. data/lib/moose_inventory/cli/group_rmchild.rb +99 -53
  20. data/lib/moose_inventory/cli/group_rmhost.rb +64 -56
  21. data/lib/moose_inventory/cli/helpers.rb +76 -0
  22. data/lib/moose_inventory/cli/host.rb +3 -0
  23. data/lib/moose_inventory/cli/host_add.rb +47 -62
  24. data/lib/moose_inventory/cli/host_addgroup.rb +73 -64
  25. data/lib/moose_inventory/cli/host_rmgroup.rb +58 -55
  26. data/lib/moose_inventory/db/db.rb +27 -7
  27. data/lib/moose_inventory/inventory_context.rb +50 -0
  28. data/lib/moose_inventory/operations/add_associations.rb +127 -0
  29. data/lib/moose_inventory/operations/add_groups.rb +115 -0
  30. data/lib/moose_inventory/operations/add_hosts.rb +110 -0
  31. data/lib/moose_inventory/operations/group_child_relations.rb +118 -0
  32. data/lib/moose_inventory/operations/group_cleanup.rb +55 -0
  33. data/lib/moose_inventory/operations/remove_associations.rb +101 -0
  34. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  35. data/lib/moose_inventory/version.rb +1 -1
  36. data/moose-inventory.gemspec +3 -0
  37. data/scripts/check.sh +2 -0
  38. data/scripts/ci/check_permissions.sh +3 -0
  39. data/scripts/ci/check_rubocop.sh +28 -0
  40. data/scripts/ci/check_secrets.sh +26 -0
  41. data/scripts/ci/check_security.sh +18 -0
  42. data/scripts/ci/install_security_tools.sh +47 -0
  43. data/scripts/install_dependencies.sh +2 -0
  44. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +40 -0
  45. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +45 -0
  46. data/spec/lib/moose_inventory/db/db_spec.rb +162 -0
  47. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +77 -0
  48. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +65 -0
  49. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +69 -0
  50. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +76 -0
  51. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +78 -0
  52. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +57 -0
  53. metadata +90 -1
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Operations
6
+ # Adds host/group associations for existing primary entities.
7
+ class AddAssociations
8
+ AUTOMATIC_GROUP = 'ungrouped'
9
+ Event = Struct.new(:type, :payload, keyword_init: true)
10
+ Result = Struct.new(:events, :warning_count, keyword_init: true)
11
+
12
+ def initialize(context:)
13
+ @context = context
14
+ end
15
+
16
+ def host_to_groups(host:, host_name:, group_names:)
17
+ events = []
18
+ warning_count = 0
19
+
20
+ group_names.each do |group_name|
21
+ next if group_name.nil? || group_name.empty?
22
+
23
+ warning_count += add_group_to_host(host, host_name, group_name, events)
24
+ end
25
+
26
+ remove_automatic_group_from_host(host, host_name, events)
27
+
28
+ Result.new(events: events, warning_count: warning_count)
29
+ end
30
+
31
+ def group_to_hosts(group:, group_name:, host_names:)
32
+ events = []
33
+ warning_count = 0
34
+ hosts_dataset = group.hosts_dataset
35
+
36
+ host_names.each do |host_name|
37
+ next if host_name.nil? || host_name.empty?
38
+
39
+ warning_count += add_host_to_group(
40
+ group,
41
+ group_name,
42
+ host_name,
43
+ hosts_dataset,
44
+ events
45
+ )
46
+ end
47
+
48
+ Result.new(events: events, warning_count: warning_count)
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :context
54
+
55
+ def add_group_to_host(host, host_name, group_name, events)
56
+ warning_count = 0
57
+ groups_dataset = host.groups_dataset
58
+
59
+ emit(events, :adding_host_group_association, host: host_name, group: group_name)
60
+
61
+ if association_exists?(groups_dataset, group_name)
62
+ emit(events, :host_group_association_exists, host: host_name, group: group_name)
63
+ emit(events, :already_exists_skipping, indent: 4)
64
+ emit(events, :ok, indent: 4)
65
+ return warning_count + 1
66
+ end
67
+
68
+ group = context.find_group(group_name)
69
+ if group.nil?
70
+ emit(events, :group_missing_created, name: group_name)
71
+ emit(events, :group_creating_now, name: group_name)
72
+ group = context.create_group(group_name)
73
+ emit(events, :ok, indent: 6)
74
+ warning_count += 1
75
+ end
76
+
77
+ host.add_group(group)
78
+ emit(events, :ok, indent: 4)
79
+ warning_count
80
+ end
81
+
82
+ def add_host_to_group(group, group_name, host_name, hosts_dataset, events)
83
+ warning_count = 0
84
+ emit(events, :adding_group_host_association, group: group_name, host: host_name)
85
+
86
+ if association_exists?(hosts_dataset, host_name)
87
+ emit(events, :group_host_association_exists, group: group_name, host: host_name)
88
+ emit(events, :already_exists_skipping, indent: 4)
89
+ emit(events, :ok, indent: 4)
90
+ return warning_count + 1
91
+ end
92
+
93
+ host = context.find_host(host_name)
94
+ if host.nil?
95
+ emit(events, :host_missing_created, name: host_name)
96
+ emit(events, :host_creating_now, name: host_name)
97
+ host = context.create_host(host_name)
98
+ emit(events, :ok, indent: 6)
99
+ warning_count += 1
100
+ end
101
+
102
+ group.add_host(host)
103
+ emit(events, :ok, indent: 4)
104
+ remove_automatic_group_from_host(host, host_name, events)
105
+ warning_count
106
+ end
107
+
108
+ def remove_automatic_group_from_host(host, host_name, events)
109
+ ungrouped = host.groups_dataset[name: AUTOMATIC_GROUP]
110
+ return if ungrouped.nil?
111
+
112
+ emit(events, :removing_automatic_group, host: host_name)
113
+ host.remove_group(ungrouped)
114
+ emit(events, :ok, indent: 4)
115
+ end
116
+
117
+ def association_exists?(dataset, name)
118
+ !dataset.nil? && !dataset[name: name].nil?
119
+ end
120
+
121
+ def emit(events, type, payload = {})
122
+ events << Event.new(type: type, payload: payload)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Operations
6
+ ##
7
+ # Adds groups and their optional host associations.
8
+ class AddGroups
9
+ AUTOMATIC_GROUP = 'ungrouped'
10
+ Event = Struct.new(:type, :payload, keyword_init: true)
11
+ Result = Struct.new(:events, :warning_count, keyword_init: true)
12
+
13
+ def initialize(context:)
14
+ @context = context
15
+ end
16
+
17
+ def call(names:, hosts:)
18
+ events = []
19
+ warning_count = 0
20
+
21
+ context.transaction do
22
+ names.each do |name|
23
+ warning_count += add_group(name, hosts, events)
24
+ end
25
+ end
26
+
27
+ Result.new(events: events, warning_count: warning_count)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :context
33
+
34
+ def add_group(name, hosts, events)
35
+ warning_count = 0
36
+ emit(events, :group_started, name: name)
37
+ group, hosts_dataset, created = create_or_find_group(name, events)
38
+ warning_count += 1 unless created
39
+
40
+ hosts.each do |host_name|
41
+ next if host_name.nil? || host_name.empty?
42
+
43
+ warning_count += add_host_association(group, name, host_name, hosts_dataset, events)
44
+ end
45
+
46
+ emit(events, :group_complete)
47
+ warning_count
48
+ end
49
+
50
+ def create_or_find_group(name, events)
51
+ emit(events, :creating_group)
52
+ group = context.find_group(name)
53
+
54
+ if group.nil?
55
+ group = context.create_group(name)
56
+ emit(events, :ok, indent: 4)
57
+ [group, nil, true]
58
+ else
59
+ emit(events, :group_exists, name: name)
60
+ emit(events, :already_exists_skipping, indent: 4)
61
+ emit(events, :ok, indent: 4)
62
+ [group, group.hosts_dataset, false]
63
+ end
64
+ end
65
+
66
+ def add_host_association(group, group_name, host_name, hosts_dataset, events)
67
+ warning_count = 0
68
+ emit(events, :adding_association, group: group_name, host: host_name)
69
+ host, created = find_or_create_host(host_name, events)
70
+ warning_count += 1 if created == :warned_create
71
+
72
+ if association_exists?(hosts_dataset, host_name)
73
+ emit(events, :association_exists, group: group_name, host: host_name)
74
+ emit(events, :already_exists_skipping, indent: 4)
75
+ warning_count += 1
76
+ else
77
+ group.add_host(host)
78
+ end
79
+ emit(events, :ok, indent: 4)
80
+
81
+ remove_automatic_group_from_host(host, host_name, events)
82
+ warning_count
83
+ end
84
+
85
+ def find_or_create_host(name, events)
86
+ host = context.find_host(name)
87
+ return [host, :existing] unless host.nil?
88
+
89
+ emit(events, :host_missing_created, name: name)
90
+ emit(events, :host_creating_now, name: name)
91
+ host = context.create_host(name)
92
+ emit(events, :ok, indent: 6)
93
+ [host, :warned_create]
94
+ end
95
+
96
+ def remove_automatic_group_from_host(host, host_name, events)
97
+ ungrouped = host.groups_dataset[name: AUTOMATIC_GROUP]
98
+ return if ungrouped.nil?
99
+
100
+ emit(events, :removing_automatic_group, host: host_name)
101
+ host.remove_group(ungrouped)
102
+ emit(events, :ok, indent: 4)
103
+ end
104
+
105
+ def association_exists?(dataset, name)
106
+ !dataset.nil? && !dataset[name: name].nil?
107
+ end
108
+
109
+ def emit(events, type, payload = {})
110
+ events << Event.new(type: type, payload: payload)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Operations
6
+ ##
7
+ # Adds hosts and their optional group associations.
8
+ #
9
+ # The operation mutates inventory state and returns structured events for
10
+ # the CLI adapter to render. Keeping output out of this class makes the
11
+ # inventory behavior easier to exercise without binding every domain test
12
+ # to progress text.
13
+ class AddHosts
14
+ AUTOMATIC_GROUP = 'ungrouped'
15
+ Event = Struct.new(:type, :payload, keyword_init: true)
16
+ Result = Struct.new(:events, keyword_init: true)
17
+
18
+ def initialize(context:)
19
+ @context = context
20
+ end
21
+
22
+ def call(names:, groups:)
23
+ events = []
24
+ context.transaction do
25
+ names.each do |name|
26
+ add_host(name, groups, events)
27
+ end
28
+ end
29
+ Result.new(events: events)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :context
35
+
36
+ def add_host(name, groups, events)
37
+ emit(events, :host_started, name: name)
38
+ host, groups_dataset = create_or_find_host(name, events)
39
+
40
+ groups.each do |group_name|
41
+ add_group_association(host, name, group_name, groups_dataset, events)
42
+ end
43
+
44
+ add_automatic_group_if_needed(host, name, events)
45
+ emit(events, :host_complete)
46
+ end
47
+
48
+ def create_or_find_host(name, events)
49
+ emit(events, :creating_host, name: name)
50
+ host = context.find_host(name)
51
+ groups_dataset = nil
52
+
53
+ if host.nil?
54
+ host = context.create_host(name)
55
+ else
56
+ emit(events, :host_exists, name: name)
57
+ groups_dataset = host.groups_dataset
58
+ end
59
+
60
+ emit(events, :ok, indent: 4)
61
+ [host, groups_dataset]
62
+ end
63
+
64
+ def add_group_association(host, host_name, group_name, groups_dataset, events)
65
+ return if group_name.nil? || group_name.empty?
66
+
67
+ emit(events, :adding_association, host: host_name, group: group_name)
68
+ group = find_or_create_group(group_name, events)
69
+
70
+ if association_exists?(groups_dataset, group_name)
71
+ emit(events, :association_exists, host: host_name, group: group_name)
72
+ else
73
+ host.add_group(group)
74
+ end
75
+
76
+ emit(events, :ok, indent: 4)
77
+ end
78
+
79
+ def find_or_create_group(name, events)
80
+ group = context.find_group(name)
81
+ return group unless group.nil?
82
+
83
+ emit(events, :group_missing_created, name: name)
84
+ context.create_group(name)
85
+ end
86
+
87
+ def add_automatic_group_if_needed(host, host_name, events)
88
+ groups_dataset = host.groups_dataset
89
+ return if groups_dataset.nil? || groups_dataset.any?
90
+
91
+ emit(events, :adding_automatic_group, host: host_name, group: AUTOMATIC_GROUP)
92
+ host.add_group(automatic_group)
93
+ emit(events, :ok, indent: 4)
94
+ end
95
+
96
+ def automatic_group
97
+ context.automatic_group
98
+ end
99
+
100
+ def association_exists?(dataset, name)
101
+ !dataset.nil? && !dataset[name: name].nil?
102
+ end
103
+
104
+ def emit(events, type, payload = {})
105
+ events << Event.new(type: type, payload: payload)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'group_cleanup'
4
+
5
+ module Moose
6
+ module Inventory
7
+ module Operations
8
+ class GroupChildRelations
9
+ Event = Struct.new(:type, :payload, keyword_init: true)
10
+ Result = Struct.new(:events, :warning_count, keyword_init: true)
11
+
12
+ def initialize(context:)
13
+ @context = context
14
+ @cleanup = Moose::Inventory::Operations::GroupCleanup.new(
15
+ context: context,
16
+ emitter: method(:emit)
17
+ )
18
+ end
19
+
20
+ def add_children(parent_group:, parent_name:, child_names:)
21
+ events = []
22
+ warning_count = 0
23
+ children_dataset = parent_group.children_dataset
24
+
25
+ child_names.each do |child_name|
26
+ next if child_name.nil? || child_name.empty?
27
+
28
+ warning_count += add_child(parent_group, parent_name, child_name, children_dataset, events)
29
+ end
30
+
31
+ Result.new(events: events, warning_count: warning_count)
32
+ end
33
+
34
+ def remove_children(parent_group:, parent_name:, child_names:, delete_orphans: false)
35
+ events = []
36
+ warning_count = 0
37
+ children_dataset = parent_group.children_dataset
38
+
39
+ child_names.each do |child_name|
40
+ next if child_name.nil? || child_name.empty?
41
+
42
+ warning_count += remove_child(
43
+ {
44
+ parent_group: parent_group,
45
+ parent_name: parent_name,
46
+ child_name: child_name,
47
+ children_dataset: children_dataset,
48
+ events: events,
49
+ delete_orphans: delete_orphans
50
+ }
51
+ )
52
+ end
53
+
54
+ Result.new(events: events, warning_count: warning_count)
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :cleanup, :context
60
+
61
+ def add_child(parent_group, parent_name, child_name, children_dataset, events)
62
+ emit(events, :adding_child_association, parent: parent_name, child: child_name)
63
+
64
+ if association_exists?(children_dataset, child_name)
65
+ emit(events, :child_association_exists, parent: parent_name, child: child_name)
66
+ emit(events, :already_exists_skipping, indent: 4)
67
+ emit(events, :ok, indent: 4)
68
+ return 1
69
+ end
70
+
71
+ child_group = context.find_group(child_name)
72
+ warning_count = 0
73
+ if child_group.nil?
74
+ emit(events, :child_group_missing, name: child_name)
75
+ emit(events, :child_group_creating_now, name: child_name)
76
+ child_group = context.create_group(child_name)
77
+ emit(events, :ok, indent: 6)
78
+ warning_count = 1
79
+ end
80
+
81
+ parent_group.add_child(child_group)
82
+ emit(events, :ok, indent: 4)
83
+ warning_count
84
+ end
85
+
86
+ def remove_child(input)
87
+ emit(
88
+ input[:events],
89
+ :removing_child_association,
90
+ parent: input[:parent_name],
91
+ child: input[:child_name]
92
+ )
93
+
94
+ unless association_exists?(input[:children_dataset], input[:child_name])
95
+ emit(input[:events], :child_association_missing, parent: input[:parent_name], child: input[:child_name])
96
+ emit(input[:events], :missing_skipping, indent: 4)
97
+ emit(input[:events], :ok, indent: 4)
98
+ return 1
99
+ end
100
+
101
+ child_group = context.find_group(input[:child_name])
102
+ input[:parent_group].remove_child(child_group)
103
+ emit(input[:events], :ok, indent: 4)
104
+ cleanup.delete_orphaned_group(child_group, input[:events]) if input[:delete_orphans]
105
+ 0
106
+ end
107
+
108
+ def association_exists?(dataset, name)
109
+ !dataset.nil? && !dataset[name: name].nil?
110
+ end
111
+
112
+ def emit(events, type, payload = {})
113
+ events << Event.new(type: type, payload: payload)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Operations
6
+ # Recursively cleans up orphaned groups and their dependent relations.
7
+ class GroupCleanup
8
+ AUTOMATIC_GROUP = 'ungrouped'
9
+
10
+ def initialize(context:, emitter:)
11
+ @context = context
12
+ @emitter = emitter
13
+ end
14
+
15
+ def delete_orphaned_group(group, events)
16
+ return if group.name == AUTOMATIC_GROUP
17
+ return unless group.parents_dataset.none?
18
+
19
+ emit(events, :recursively_delete_orphaned_group, name: group.name)
20
+ group.children_dataset.each do |child|
21
+ emit(events, :removing_recursive_child_association, parent: group.name, child: child.name)
22
+ group.remove_child(child)
23
+ emit(events, :ok, indent: 6)
24
+ delete_orphaned_group(child, events)
25
+ end
26
+ destroy_group(group, events, indent: 4)
27
+ end
28
+
29
+ def destroy_group(group, events, indent:)
30
+ group.hosts_dataset.each do |host|
31
+ next unless host.groups_dataset.one?
32
+
33
+ emit(events, :adding_automatic_group_to_host, host: host[:name], indent: indent)
34
+ host.add_group(context.automatic_group)
35
+ emit(events, :ok, indent: indent + 2)
36
+ end
37
+
38
+ emit(events, :destroying_group, name: group.name, indent: indent)
39
+ group.remove_all_groupvars
40
+ group.remove_all_hosts
41
+ group.destroy
42
+ emit(events, :ok, indent: indent + 2)
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :context, :emitter
48
+
49
+ def emit(events, type, payload = {})
50
+ emitter.call(events, type, payload)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moose
4
+ module Inventory
5
+ module Operations
6
+ # Removes host/group associations for existing primary entities.
7
+ class RemoveAssociations
8
+ AUTOMATIC_GROUP = 'ungrouped'
9
+ Event = Struct.new(:type, :payload, keyword_init: true)
10
+ Result = Struct.new(:events, :warning_count, keyword_init: true)
11
+
12
+ def initialize(context:)
13
+ @context = context
14
+ end
15
+
16
+ def host_from_groups(host:, host_name:, group_names:)
17
+ events = []
18
+ warning_count = 0
19
+
20
+ group_names.each do |group_name|
21
+ next if group_name.nil? || group_name.empty?
22
+
23
+ warning_count += remove_group_from_host(host, host_name, group_name, events)
24
+ end
25
+
26
+ add_automatic_group_if_needed(host, host_name, events)
27
+
28
+ Result.new(events: events, warning_count: warning_count)
29
+ end
30
+
31
+ def group_from_hosts(group:, group_name:, host_names:)
32
+ events = []
33
+ warning_count = 0
34
+ hosts_dataset = group.hosts_dataset
35
+
36
+ host_names.each do |host_name|
37
+ next if host_name.nil? || host_name.empty?
38
+
39
+ warning_count += remove_host_from_group(group, group_name, host_name, hosts_dataset, events)
40
+ end
41
+
42
+ Result.new(events: events, warning_count: warning_count)
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :context
48
+
49
+ def remove_group_from_host(host, host_name, group_name, events)
50
+ groups_dataset = host.groups_dataset
51
+ emit(events, :removing_host_group_association, host: host_name, group: group_name)
52
+
53
+ unless association_exists?(groups_dataset, group_name)
54
+ emit(events, :host_group_association_missing, host: host_name, group: group_name)
55
+ emit(events, :missing_skipping, indent: 4)
56
+ emit(events, :ok, indent: 4)
57
+ return 1
58
+ end
59
+
60
+ group = context.find_group(group_name)
61
+ host.remove_group(group) unless group.nil?
62
+ emit(events, :ok, indent: 4)
63
+ 0
64
+ end
65
+
66
+ def remove_host_from_group(group, group_name, host_name, hosts_dataset, events)
67
+ emit(events, :removing_group_host_association, group: group_name, host: host_name)
68
+
69
+ unless association_exists?(hosts_dataset, host_name)
70
+ emit(events, :group_host_association_missing, group: group_name, host: host_name)
71
+ emit(events, :missing_skipping, indent: 4)
72
+ emit(events, :ok, indent: 4)
73
+ return 1
74
+ end
75
+
76
+ host = context.find_host(host_name)
77
+ group.remove_host(host) unless host.nil?
78
+ emit(events, :ok, indent: 4)
79
+ add_automatic_group_if_needed(host, host_name, events)
80
+ 0
81
+ end
82
+
83
+ def add_automatic_group_if_needed(host, host_name, events)
84
+ return unless host.groups_dataset.none?
85
+
86
+ emit(events, :adding_automatic_group, host: host_name)
87
+ host.add_group(context.automatic_group)
88
+ emit(events, :ok, indent: 4)
89
+ end
90
+
91
+ def association_exists?(dataset, name)
92
+ !dataset.nil? && !dataset[name: name].nil?
93
+ end
94
+
95
+ def emit(events, type, payload = {})
96
+ events << Event.new(type: type, payload: payload)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'group_cleanup'
4
+
5
+ module Moose
6
+ module Inventory
7
+ module Operations
8
+ # Removes top-level groups and their direct associations.
9
+ class RemoveGroups
10
+ Event = Struct.new(:type, :payload, keyword_init: true)
11
+ Result = Struct.new(:events, :warning_count, keyword_init: true)
12
+
13
+ def initialize(context:)
14
+ @context = context
15
+ @cleanup = Moose::Inventory::Operations::GroupCleanup.new(
16
+ context: context,
17
+ emitter: method(:emit)
18
+ )
19
+ end
20
+
21
+ def call(names:, recursive: false)
22
+ events = []
23
+ warning_count = 0
24
+
25
+ names.each do |name|
26
+ warning_count += remove_group(name, events, recursive: recursive)
27
+ end
28
+
29
+ Result.new(events: events, warning_count: warning_count)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :cleanup, :context
35
+
36
+ def remove_group(name, events, recursive:)
37
+ emit(events, :group_started, name: name)
38
+ emit(events, :retrieving_group, name: name)
39
+ group = context.find_group(name)
40
+
41
+ if group.nil?
42
+ emit(events, :group_missing, name: name)
43
+ emit(events, :ok, indent: 4)
44
+ emit(events, :group_complete)
45
+ return 1
46
+ end
47
+
48
+ emit(events, :ok, indent: 4)
49
+ remove_parent_associations(group, name, events)
50
+ remove_child_associations(group, name, events, recursive: recursive)
51
+ cleanup.destroy_group(group, events, indent: 2)
52
+ emit(events, :group_complete)
53
+ 0
54
+ end
55
+
56
+ def remove_parent_associations(group, name, events)
57
+ group.parents_dataset.each do |parent|
58
+ emit(events, :removing_parent_association, group: name, related_group: parent.name)
59
+ parent.remove_child(group)
60
+ emit(events, :ok, indent: 4)
61
+ end
62
+ end
63
+
64
+ def remove_child_associations(group, name, events, recursive:)
65
+ group.children_dataset.each do |child|
66
+ emit(events, :removing_child_association, group: name, related_group: child.name)
67
+ group.remove_child(child)
68
+ emit(events, :ok, indent: 4)
69
+ cleanup.delete_orphaned_group(child, events) if recursive
70
+ end
71
+ end
72
+
73
+ def emit(events, type, payload = {})
74
+ events << Event.new(type: type, payload: payload)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end