moose-inventory 1.0.9 → 2.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 (176) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -1
  3. data/.github/workflows/release.yml +60 -0
  4. data/.gitignore +2 -1
  5. data/.gitleaks.toml +9 -0
  6. data/.rubocop.yml +49 -0
  7. data/BACKLOG.md +752 -24
  8. data/Gemfile +2 -0
  9. data/Gemfile.lock +36 -1
  10. data/README.md +340 -44
  11. data/Rakefile +2 -0
  12. data/bin/moose-inventory +2 -1
  13. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  14. data/docs/compatibility/cli-output-compatibility.md +76 -0
  15. data/docs/governance/approval-register.md +37 -0
  16. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  17. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  18. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  19. data/docs/product/product-brief.md +161 -0
  20. data/docs/product/requirements-baseline.md +477 -0
  21. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  22. data/docs/release/package-provenance-hardening.md +126 -0
  23. data/docs/release/publishing.md +54 -50
  24. data/docs/release/release-environment-protection.md +70 -0
  25. data/docs/release/release-readiness.md +37 -4
  26. data/docs/security/accepted-risk-register.md +84 -0
  27. data/docs/security/security-privacy-process.md +287 -0
  28. data/docs/security-audit-2026-05-26-rerun.md +75 -0
  29. data/docs/security-audit-2026-05-26.md +63 -0
  30. data/docs/ux/cli-workflow-notes.md +287 -0
  31. data/examples/ansible/ansible.cfg +3 -0
  32. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  33. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  34. data/examples/ci/README.md +16 -0
  35. data/examples/ci/github-actions/inventory-review.yml +38 -0
  36. data/examples/ci/inventory/example-snapshot.yml +19 -0
  37. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  38. data/lib/moose_inventory/cli/application.rb +133 -5
  39. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  40. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  41. data/lib/moose_inventory/cli/audit.rb +62 -0
  42. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  43. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  44. data/lib/moose_inventory/cli/console.rb +135 -0
  45. data/lib/moose_inventory/cli/db.rb +64 -0
  46. data/lib/moose_inventory/cli/factory.rb +28 -0
  47. data/lib/moose_inventory/cli/formatter.rb +8 -12
  48. data/lib/moose_inventory/cli/group.rb +7 -1
  49. data/lib/moose_inventory/cli/group_add.rb +91 -73
  50. data/lib/moose_inventory/cli/group_addchild.rb +41 -66
  51. data/lib/moose_inventory/cli/group_addhost.rb +33 -71
  52. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  53. data/lib/moose_inventory/cli/group_get.rb +8 -42
  54. data/lib/moose_inventory/cli/group_list.rb +7 -40
  55. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  56. data/lib/moose_inventory/cli/group_rm.rb +105 -73
  57. data/lib/moose_inventory/cli/group_rmchild.rb +47 -57
  58. data/lib/moose_inventory/cli/group_rmhost.rb +34 -61
  59. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  60. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  61. data/lib/moose_inventory/cli/helpers.rb +143 -0
  62. data/lib/moose_inventory/cli/host.rb +8 -2
  63. data/lib/moose_inventory/cli/host_add.rb +91 -66
  64. data/lib/moose_inventory/cli/host_addgroup.rb +39 -66
  65. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  66. data/lib/moose_inventory/cli/host_get.rb +9 -37
  67. data/lib/moose_inventory/cli/host_list.rb +24 -21
  68. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  69. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  70. data/lib/moose_inventory/cli/host_rmgroup.rb +39 -55
  71. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  72. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  73. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  74. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  75. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  76. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  77. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  78. data/lib/moose_inventory/config/config.rb +185 -108
  79. data/lib/moose_inventory/db/db.rb +188 -193
  80. data/lib/moose_inventory/db/exceptions.rb +6 -3
  81. data/lib/moose_inventory/db/models.rb +16 -0
  82. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  83. data/lib/moose_inventory/inventory_context.rb +116 -0
  84. data/lib/moose_inventory/operations/add_associations.rb +131 -0
  85. data/lib/moose_inventory/operations/add_groups.rb +123 -0
  86. data/lib/moose_inventory/operations/add_hosts.rb +123 -0
  87. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  88. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  89. data/lib/moose_inventory/operations/group_child_relations.rb +125 -0
  90. data/lib/moose_inventory/operations/group_cleanup.rb +70 -0
  91. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  92. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  94. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  95. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  96. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
  97. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  98. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  99. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  100. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  101. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  102. data/lib/moose_inventory/operations/remove_associations.rb +113 -0
  103. data/lib/moose_inventory/operations/remove_groups.rb +79 -0
  104. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  105. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  106. data/lib/moose_inventory/runtime_options.rb +31 -0
  107. data/lib/moose_inventory/version.rb +3 -1
  108. data/lib/moose_inventory.rb +10 -7
  109. data/moose-inventory.gemspec +22 -35
  110. data/scripts/check.sh +3 -0
  111. data/scripts/ci/check_generated_artifacts.sh +41 -0
  112. data/scripts/ci/check_permissions.sh +5 -0
  113. data/scripts/ci/check_rubocop.sh +33 -0
  114. data/scripts/ci/check_secrets.sh +26 -0
  115. data/scripts/ci/check_security.sh +18 -0
  116. data/scripts/ci/install_security_tools.sh +47 -0
  117. data/scripts/files.rb +5 -4
  118. data/scripts/install_dependencies.sh +2 -0
  119. data/spec/examples/ci_examples_spec.rb +37 -0
  120. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  121. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  122. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
  123. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  124. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  125. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  126. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  127. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  128. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  129. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  130. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  131. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  132. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  133. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  134. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  135. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  136. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +165 -85
  137. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +100 -30
  138. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  139. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  140. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  141. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  142. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  143. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  144. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  145. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  146. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  147. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  148. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  149. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  150. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  151. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  152. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  153. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  154. data/spec/lib/moose_inventory/db/db_spec.rb +551 -29
  155. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  156. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  157. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  158. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  159. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +111 -0
  160. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +80 -0
  161. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +82 -0
  162. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  163. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +122 -0
  164. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
  165. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  166. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  167. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  168. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  169. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +113 -0
  170. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +78 -0
  171. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  172. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  173. data/spec/shared/shared_config_setup.rb +4 -3
  174. data/spec/spec_helper.rb +50 -40
  175. data/spec/support/cli_harness.rb +33 -0
  176. metadata +163 -35
@@ -1,51 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
1
4
  require 'spec_helper'
2
5
 
3
- # TODO: the usual respond_to? method doesn't seem to work on Thor objects.
4
6
  # Why not? For now, we'll check against instance_methods.
5
7
 
6
8
  RSpec.describe Moose::Inventory::Cli::Group do
7
9
  before(:all) do
8
- # Set up the configuration object
9
- @mockarg_parts = {
10
- config: File.join(spec_root, 'config/config.yml'),
11
- format: 'yaml',
12
- env: 'test',
13
- }
14
-
15
- @mockargs = []
16
- @mockarg_parts.each do |key, val|
17
- @mockargs << "--#{key}"
18
- @mockargs << val
19
- end
20
-
21
- @config = Moose::Inventory::Config
22
- @config.init(@mockargs)
23
-
24
- @console = Moose::Inventory::Cli::Formatter
25
-
26
- @db = Moose::Inventory::DB
27
- @db.init if @db.db.nil?
28
-
29
- @group = Moose::Inventory::Cli::Group
30
- @host = Moose::Inventory::Cli::Host
31
- @app = Moose::Inventory::Cli::Application
10
+ setup_cli_harness(
11
+ command_class: Moose::Inventory::Cli::Group,
12
+ command_ivar: :@group,
13
+ extra_commands: { :@host => Moose::Inventory::Cli::Host }
14
+ )
32
15
  end
33
16
 
34
17
  before(:each) do
35
- @db.reset
18
+ reset_cli_harness
36
19
  end
37
20
 
38
21
  #======================
39
22
  describe 'rm' do
40
23
  #---------------
41
24
  it 'Group.rm() should be responsive' do
42
- result = @group.instance_methods(false).include?(:rm)
25
+ result = @group.method_defined?(:rm, false)
43
26
  expect(result).to eq(true)
44
27
  end
45
28
 
46
29
  #---------------
47
30
  it '<missing argument> ... should abort with an error' do
48
- actual = runner { @app.start(%w(host rm)) }
31
+ actual = runner { @app.start(%w[host rm]) }
49
32
 
50
33
  # Check output
51
34
  desired = { aborted: true, STDERR: '', STDOUT: '' }
@@ -55,7 +38,7 @@ RSpec.describe Moose::Inventory::Cli::Group do
55
38
 
56
39
  # --------------------
57
40
  it 'ungrouped ... should abort with an error' do
58
- actual = runner { @app.start(%w(group rm ungrouped)) }
41
+ actual = runner { @app.start(%w[group rm ungrouped]) }
59
42
 
60
43
  # Check output
61
44
  desired = { aborted: true }
@@ -73,16 +56,16 @@ RSpec.describe Moose::Inventory::Cli::Group do
73
56
 
74
57
  # no items in the db
75
58
  group_name = 'fake'
76
- actual = runner { @app.start(%W(group rm #{group_name})) }
59
+ actual = runner { @app.start(%W[group rm #{group_name} --yes]) }
77
60
 
78
61
  # @console.out(actual,'y')
79
62
  desired = {}
80
63
  desired[:STDOUT] =
81
- "Remove group '#{group_name}':\n"\
82
- " - Retrieve group '#{group_name}'...\n"\
83
- " - No such group, skipping.\n"\
84
- " - OK\n"\
85
- " - All OK\n"\
64
+ "Remove group '#{group_name}':\n " \
65
+ "- Retrieve group '#{group_name}'...\n " \
66
+ "- No such group, skipping.\n " \
67
+ "- OK\n " \
68
+ "- All OK\n" \
86
69
  "Succeeded, with warnings.\n"
87
70
  desired[:STDERR] =
88
71
  "WARNING: Group '#{group_name}' does not exist, skipping.\n"
@@ -95,17 +78,17 @@ RSpec.describe Moose::Inventory::Cli::Group do
95
78
  group_name = 'test1'
96
79
  @db.models[:group].create(name: group_name)
97
80
 
98
- actual = runner { @app.start(%W(group rm #{group_name})) }
81
+ actual = runner { @app.start(%W[group rm #{group_name} --yes]) }
99
82
 
100
83
  # Check output
101
84
  desired = {}
102
85
  desired[:STDOUT] =
103
- "Remove group '#{group_name}':\n"\
104
- " - Retrieve group '#{group_name}'...\n"\
105
- " - OK\n"\
106
- " - Destroy group '#{group_name}'...\n"\
107
- " - OK\n"\
108
- " - All OK\n"\
86
+ "Remove group '#{group_name}':\n " \
87
+ "- Retrieve group '#{group_name}'...\n " \
88
+ "- OK\n " \
89
+ "- Destroy group '#{group_name}'...\n " \
90
+ "- OK\n " \
91
+ "- All OK\n" \
109
92
  "Succeeded.\n"
110
93
  expected(actual, desired)
111
94
 
@@ -114,12 +97,68 @@ RSpec.describe Moose::Inventory::Cli::Group do
114
97
  expect(group).to be_nil
115
98
  end
116
99
 
100
+ #---------------
101
+ it 'GROUP without --yes or --dry-run should abort before deleting' do
102
+ group_name = 'group1'
103
+ @db.models[:group].create(name: group_name)
104
+
105
+ actual = runner { @app.start(%W[group rm #{group_name}]) }
106
+
107
+ desired = {
108
+ aborted: true,
109
+ STDOUT: '',
110
+ STDERR: "ERROR: group rm #{group_name} is destructive. Re-run with --yes to confirm, " \
111
+ "or use --dry-run to preview.\n"
112
+ }
113
+ expected(actual, desired)
114
+ expect(@db.models[:group].find(name: group_name)).not_to be_nil
115
+ end
116
+
117
+ #---------------
118
+ it 'GROUP --dry-run should show planned removal without deleting the group' do
119
+ group_name = 'test1'
120
+ @db.models[:group].create(name: group_name)
121
+
122
+ actual = runner { @app.start(%W[group rm #{group_name} --dry-run]) }
123
+
124
+ desired = {}
125
+ desired[:STDOUT] =
126
+ "Remove group '#{group_name}':\n " \
127
+ "- Retrieve group '#{group_name}'...\n " \
128
+ "- OK\n " \
129
+ "- Destroy group '#{group_name}'...\n " \
130
+ "- OK\n " \
131
+ "- All OK\n" \
132
+ "Dry run complete. No changes applied.\n" \
133
+ "Succeeded.\n"
134
+
135
+ expected(actual, desired)
136
+ expect(@db.models[:group].find(name: group_name)).not_to be_nil
137
+ end
138
+
139
+ #---------------
140
+ it 'GROUP --recursive --dry-run should show recursive cleanup without deleting groups' do
141
+ runner { @app.start(%w[group add parent]) }
142
+ runner { @app.start(%w[group add child --hosts child-host]) }
143
+ runner { @app.start(%w[group addchild parent child]) }
144
+
145
+ actual = runner { @app.start(%w[group rm --recursive parent --dry-run]) }
146
+
147
+ expect(actual[:unexpected]).to eq(false)
148
+ expect(actual[:aborted]).to eq(false)
149
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'child'...\n")
150
+ expect(actual[:STDOUT]).to include('Dry run complete. No changes applied.')
151
+ expect(@db.models[:group].find(name: 'parent')).not_to be_nil
152
+ expect(@db.models[:group].find(name: 'child')).not_to be_nil
153
+ host = @db.models[:host].find(name: 'child-host')
154
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
155
+ end
117
156
  #---------------
118
157
  it "GROUP ... should handle the automatic 'ungrouped' group for associated hosts" do
119
158
  host_name = 'test-host1'
120
159
  group_name = 'test-group1'
121
160
 
122
- tmp = runner { @app.start(%W(group add #{group_name} --hosts #{host_name})) }
161
+ tmp = runner { @app.start(%W[group add #{group_name} --hosts #{host_name}]) }
123
162
  expect(tmp[:unexpected]).to eq(false)
124
163
  expect(tmp[:aborted]).to eq(false)
125
164
  host = @db.models[:host].find(name: host_name)
@@ -128,21 +167,21 @@ RSpec.describe Moose::Inventory::Cli::Group do
128
167
  expect(groups_ds[name: 'ungrouped']).to be_nil # Shouldn't be ungrouped
129
168
 
130
169
  # Now do the rm
131
- actual = runner { @app.start(%W(group rm #{group_name})) }
170
+ actual = runner { @app.start(%W[group rm #{group_name} --yes]) }
132
171
 
133
172
  # @console.out(actual)
134
173
 
135
174
  # Check output
136
175
  desired = {}
137
176
  desired[:STDOUT] =
138
- "Remove group '#{group_name}':\n"\
139
- " - Retrieve group '#{group_name}'...\n"\
140
- " - OK\n"\
141
- " - Adding automatic association {group:ungrouped <-> host:#{host_name}}...\n"\
142
- " - OK\n"\
143
- " - Destroy group '#{group_name}'...\n"\
144
- " - OK\n"\
145
- " - All OK\n"\
177
+ "Remove group '#{group_name}':\n " \
178
+ "- Retrieve group '#{group_name}'...\n " \
179
+ "- OK\n " \
180
+ "- Adding automatic association {group:ungrouped <-> host:#{host_name}}...\n " \
181
+ "- OK\n " \
182
+ "- Destroy group '#{group_name}'...\n " \
183
+ "- OK\n " \
184
+ "- All OK\n" \
146
185
  "Succeeded.\n"
147
186
  expected(actual, desired)
148
187
 
@@ -159,26 +198,26 @@ RSpec.describe Moose::Inventory::Cli::Group do
159
198
 
160
199
  #---------------
161
200
  it 'GROUP1 GROUP2 ... should remove multiple groups' do
162
- names = %w(group1 group2 group3)
201
+ names = %w[group1 group2 group3]
163
202
  names.each do |name|
164
203
  @db.models[:group].create(name: name)
165
204
  end
166
205
 
167
- actual = runner { @app.start(%w(group rm) + names) }
206
+ actual = runner { @app.start(%w[group rm --yes] + names) }
168
207
 
169
208
  # Check output
170
209
  desired = { STDOUT: '' }
171
210
  names.each do |name|
172
211
  # Check output
173
212
  desired[:STDOUT] = desired[:STDOUT] +
174
- "Remove group '#{name}':\n"\
175
- " - Retrieve group '#{name}'...\n"\
176
- " - OK\n"\
177
- " - Destroy group '#{name}'...\n"\
178
- " - OK\n"\
179
- " - All OK\n"
213
+ "Remove group '#{name}':\n " \
214
+ "- Retrieve group '#{name}'...\n " \
215
+ "- OK\n " \
216
+ "- Destroy group '#{name}'...\n " \
217
+ "- OK\n " \
218
+ "- All OK\n"
180
219
  end
181
- desired[:STDOUT] = desired[:STDOUT] + "Succeeded.\n"
220
+ desired[:STDOUT] = "#{desired[:STDOUT]}Succeeded.\n"
182
221
  expected(actual, desired)
183
222
 
184
223
  # Check db
@@ -189,24 +228,24 @@ RSpec.describe Moose::Inventory::Cli::Group do
189
228
  #---------------
190
229
  it 'GROUP ... should remove GROUP, where GROUP has an associated parent.' do
191
230
  @db.models[:group].create(name: 'parent')
192
- runner { @app.start(%w(group addchild parent child)) }
231
+ runner { @app.start(%w[group addchild parent child]) }
193
232
 
194
- actual = runner { @app.start(%w(group rm child)) }
233
+ actual = runner { @app.start(%w[group rm child --yes]) }
195
234
 
196
235
  # Check output
197
236
  desired = { STDOUT: '' }
198
237
  # Check output
199
238
  desired[:STDOUT] = desired[:STDOUT] +
200
- "Remove group 'child':\n"\
201
- " - Retrieve group 'child'...\n"\
202
- " - OK\n"\
203
- " - Remove association {group:child <-> group:parent}...\n"\
204
- " - OK\n"\
205
- " - Destroy group 'child'...\n"\
206
- " - OK\n"\
207
- " - All OK\n"
208
-
209
- desired[:STDOUT] = desired[:STDOUT] + "Succeeded.\n"
239
+ "Remove group 'child':\n " \
240
+ "- Retrieve group 'child'...\n " \
241
+ "- OK\n " \
242
+ "- Remove association {group:child <-> group:parent}...\n " \
243
+ "- OK\n " \
244
+ "- Destroy group 'child'...\n " \
245
+ "- OK\n " \
246
+ "- All OK\n"
247
+
248
+ desired[:STDOUT] = "#{desired[:STDOUT]}Succeeded.\n"
210
249
  expected(actual, desired)
211
250
 
212
251
  # Check db
@@ -217,29 +256,70 @@ RSpec.describe Moose::Inventory::Cli::Group do
217
256
  #---------------
218
257
  it 'GROUP ... should remove GROUP, where GROUP has an associated child.' do
219
258
  @db.models[:group].create(name: 'parent')
220
- runner { @app.start(%w(group addchild parent child)) }
259
+ runner { @app.start(%w[group addchild parent child]) }
221
260
 
222
- actual = runner { @app.start(%w(group rm parent)) }
261
+ actual = runner { @app.start(%w[group rm parent --yes]) }
223
262
 
224
263
  # Check output
225
264
  desired = { STDOUT: '' }
226
265
  # Check output
227
266
  desired[:STDOUT] = desired[:STDOUT] +
228
- "Remove group 'parent':\n"\
229
- " - Retrieve group 'parent'...\n"\
230
- " - OK\n"\
231
- " - Remove association {group:parent <-> group:child}...\n"\
232
- " - OK\n"\
233
- " - Destroy group 'parent'...\n"\
234
- " - OK\n"\
235
- " - All OK\n"
236
-
237
- desired[:STDOUT] = desired[:STDOUT] + "Succeeded.\n"
267
+ "Remove group 'parent':\n " \
268
+ "- Retrieve group 'parent'...\n " \
269
+ "- OK\n " \
270
+ "- Remove association {group:parent <-> group:child}...\n " \
271
+ "- OK\n " \
272
+ "- Destroy group 'parent'...\n " \
273
+ "- OK\n " \
274
+ "- All OK\n"
275
+
276
+ desired[:STDOUT] = "#{desired[:STDOUT]}Succeeded.\n"
238
277
  expected(actual, desired)
239
278
 
240
279
  # Check db
241
280
  groups = @db.models[:group].all
242
281
  expect(groups.count).to eq(1)
243
282
  end
283
+
284
+ #---------------
285
+ it 'GROUP --recursive ... should remove orphaned child groups recursively' do
286
+ runner { @app.start(%w[group add parent]) }
287
+ runner { @app.start(%w[group add child --hosts child-host]) }
288
+ runner { @app.start(%w[group add grandchild]) }
289
+ runner { @app.start(%w[group addchild parent child]) }
290
+ runner { @app.start(%w[group addchild child grandchild]) }
291
+
292
+ actual = runner { @app.start(%w[group rm --recursive parent --yes]) }
293
+
294
+ expect(actual[:unexpected]).to eq(false)
295
+ expect(actual[:aborted]).to eq(false)
296
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'child'...\n")
297
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'grandchild'...\n")
298
+
299
+ %w[parent child grandchild].each do |name|
300
+ expect(@db.models[:group].find(name: name)).to be_nil
301
+ end
302
+
303
+ host = @db.models[:host].find(name: 'child-host')
304
+ expect(host.groups_dataset[name: 'ungrouped']).not_to be_nil
305
+ end
306
+
307
+ #---------------
308
+ it 'GROUP --recursive ... should not remove child groups with another parent' do
309
+ runner { @app.start(%w[group add parent other-parent]) }
310
+ runner { @app.start(%w[group addchild parent child]) }
311
+ runner { @app.start(%w[group addchild other-parent child]) }
312
+
313
+ actual = runner { @app.start(%w[group rm --recursive parent --yes]) }
314
+
315
+ expect(actual[:unexpected]).to eq(false)
316
+ expect(actual[:aborted]).to eq(false)
317
+ expect(@db.models[:group].find(name: 'parent')).to be_nil
318
+
319
+ child = @db.models[:group].find(name: 'child')
320
+ expect(child).not_to be_nil
321
+ expect(child.parents_dataset[name: 'other-parent']).not_to be_nil
322
+ end
244
323
  end
245
324
  end
325
+ # rubocop:enable Metrics/BlockLength
@@ -1,15 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
1
4
  require 'spec_helper'
2
5
 
3
- # TODO: the usual respond_to? method doesn't seem to work on Thor objects.
4
6
  # Why not? For now, we'll check against instance_methods.
5
7
 
6
8
  RSpec.describe Moose::Inventory::Cli::Group do
7
9
  before(:all) do
8
10
  # Set up the configuration object
9
11
  @mockarg_parts = {
10
- config: File.join(spec_root, 'config/config.yml'),
11
- format: 'yaml',
12
- env: 'test',
12
+ config: File.join(spec_root, 'config/config.yml'),
13
+ format: 'yaml',
14
+ env: 'test'
13
15
  }
14
16
 
15
17
  @mockargs = []
@@ -39,14 +41,14 @@ RSpec.describe Moose::Inventory::Cli::Group do
39
41
  describe 'rmchild' do
40
42
  #------------------------
41
43
  it 'Group.rmchild() should be responsive' do
42
- result = @group.instance_methods(false).include?(:rmchild)
44
+ result = @group.method_defined?(:rmchild, false)
43
45
  expect(result).to eq(true)
44
46
  end
45
47
 
46
48
  #------------------------
47
49
  it '<missing args> ... should abort with an error' do
48
50
  actual = runner do
49
- @app.start(%w(group addchild))
51
+ @app.start(%w[group addchild])
50
52
  end
51
53
 
52
54
  # @console.out(actual, 'y')
@@ -63,7 +65,7 @@ RSpec.describe Moose::Inventory::Cli::Group do
63
65
  child_name = 'fake'
64
66
 
65
67
  actual = runner do
66
- @app.start(%W(group addchild #{parent_name} #{child_name}))
68
+ @app.start(%W[group addchild #{parent_name} #{child_name}])
67
69
  end
68
70
 
69
71
  # @console.out(actual, 'y')
@@ -79,7 +81,7 @@ RSpec.describe Moose::Inventory::Cli::Group do
79
81
  child_name = 'ungrouped'
80
82
 
81
83
  actual = runner do
82
- @app.start(%W(group rmchild #{parent_name} #{child_name}))
84
+ @app.start(%W[group rmchild #{parent_name} #{child_name} --yes])
83
85
  end
84
86
 
85
87
  # @console.out(actual, 'y')
@@ -96,17 +98,17 @@ RSpec.describe Moose::Inventory::Cli::Group do
96
98
  cname = 'child group'
97
99
 
98
100
  actual = runner do
99
- @app.start(%W(group rmchild #{pname} #{cname}))
101
+ @app.start(%W[group rmchild #{pname} #{cname} --yes])
100
102
  end
101
103
 
102
104
  # @console.out(actual, 'y')
103
105
  # Check output
104
106
  desired = { aborted: true }
105
107
  desired[:STDOUT] =
106
- "Dissociate parent group '#{pname}' from child group(s) '#{cname}':\n"\
107
- " - retrieve group '#{pname}'...\n"
108
+ "Dissociate parent group '#{pname}' from child group(s) '#{cname}':\n " \
109
+ "- retrieve group '#{pname}'...\n"
108
110
  desired[:STDERR] =
109
- "ERROR: The group '#{pname}' does not exist.\n"\
111
+ "ERROR: The group '#{pname}' does not exist.\n" \
110
112
  "An error occurred during a transaction, any changes have been rolled back.\n"
111
113
  expected(actual, desired)
112
114
  end
@@ -116,10 +118,10 @@ RSpec.describe Moose::Inventory::Cli::Group do
116
118
  pname = 'parent_group'
117
119
  cname = 'child group'
118
120
 
119
- runner { @app.start(%W(group add #{pname} #{cname})) }
121
+ runner { @app.start(%W[group add #{pname} #{cname}]) }
120
122
 
121
123
  actual = runner do
122
- @app.start(%W(group rmchild #{pname} #{cname}))
124
+ @app.start(%W[group rmchild #{pname} #{cname} --yes])
123
125
  end
124
126
 
125
127
  # @console.out(actual, 'y')
@@ -127,13 +129,13 @@ RSpec.describe Moose::Inventory::Cli::Group do
127
129
  # Check output
128
130
  desired = {}
129
131
  desired[:STDOUT] =
130
- "Dissociate parent group '#{pname}' from child group(s) '#{cname}':\n"\
131
- " - retrieve group '#{pname}'...\n"\
132
- " - OK\n"\
133
- " - remove association {group:#{pname} <-> group:#{cname}}...\n"\
134
- " - doesn't exist, skipping.\n"\
135
- " - OK\n"\
136
- " - all OK\n"\
132
+ "Dissociate parent group '#{pname}' from child group(s) '#{cname}':\n " \
133
+ "- retrieve group '#{pname}'...\n " \
134
+ "- OK\n " \
135
+ "- remove association {group:#{pname} <-> group:#{cname}}...\n " \
136
+ "- doesn't exist, skipping.\n " \
137
+ "- OK\n " \
138
+ "- all OK\n" \
137
139
  "Succeeded, with warnings.\n"
138
140
 
139
141
  desired[:STDERR] =
@@ -147,11 +149,11 @@ RSpec.describe Moose::Inventory::Cli::Group do
147
149
  pname = 'parent_group'
148
150
  cname = 'child group'
149
151
 
150
- runner { @app.start(%W(group add #{pname} #{cname})) }
151
- runner { @app.start(%W(group addchild #{pname} #{cname})) }
152
+ runner { @app.start(%W[group add #{pname} #{cname}]) }
153
+ runner { @app.start(%W[group addchild #{pname} #{cname}]) }
152
154
 
153
155
  actual = runner do
154
- @app.start(%W(group rmchild #{pname} #{cname}))
156
+ @app.start(%W[group rmchild #{pname} #{cname} --yes])
155
157
  end
156
158
 
157
159
  # @console.out(actual, 'y')
@@ -159,15 +161,83 @@ RSpec.describe Moose::Inventory::Cli::Group do
159
161
  # Check output
160
162
  desired = {}
161
163
  desired[:STDOUT] =
162
- "Dissociate parent group '#{pname}' from child group(s) '#{cname}':\n"\
163
- " - retrieve group '#{pname}'...\n"\
164
- " - OK\n"\
165
- " - remove association {group:#{pname} <-> group:#{cname}}...\n"\
166
- " - OK\n"\
167
- " - all OK\n"\
164
+ "Dissociate parent group '#{pname}' from child group(s) '#{cname}':\n " \
165
+ "- retrieve group '#{pname}'...\n " \
166
+ "- OK\n " \
167
+ "- remove association {group:#{pname} <-> group:#{cname}}...\n " \
168
+ "- OK\n " \
169
+ "- all OK\n" \
168
170
  "Succeeded.\n"
169
171
 
170
172
  expected(actual, desired)
171
173
  end
174
+
175
+ #------------------------
176
+ it 'GROUP CHILDGROUP --delete-orphans --dry-run should not remove or delete groups' do
177
+ runner { @app.start(%w[group add parent]) }
178
+ runner { @app.start(%w[group add child --hosts child-host]) }
179
+ runner { @app.start(%w[group addchild parent child]) }
180
+
181
+ actual = runner do
182
+ @app.start(%w[group rmchild --delete-orphans parent child --dry-run])
183
+ end
184
+
185
+ expect(actual[:unexpected]).to eq(false)
186
+ expect(actual[:aborted]).to eq(false)
187
+ expect(actual[:STDOUT]).to include('Dry run complete. No changes applied.')
188
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'child'...
189
+ ")
190
+ parent = @db.models[:group].find(name: 'parent')
191
+ expect(parent.children_dataset[name: 'child']).not_to be_nil
192
+ expect(@db.models[:group].find(name: 'child')).not_to be_nil
193
+ host = @db.models[:host].find(name: 'child-host')
194
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
195
+ end
196
+
197
+ #------------------------
198
+ it 'GROUP CHILDGROUP --delete-orphans ... should delete orphaned child groups recursively' do
199
+ runner { @app.start(%w[group add parent]) }
200
+ runner { @app.start(%w[group add child --hosts child-host]) }
201
+ runner { @app.start(%w[group add grandchild]) }
202
+ runner { @app.start(%w[group addchild parent child]) }
203
+ runner { @app.start(%w[group addchild child grandchild]) }
204
+
205
+ actual = runner do
206
+ @app.start(%w[group rmchild --delete-orphans parent child --yes])
207
+ end
208
+
209
+ expect(actual[:unexpected]).to eq(false)
210
+ expect(actual[:aborted]).to eq(false)
211
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'child'...\n")
212
+ expect(actual[:STDOUT]).to include("- Recursively delete orphaned group 'grandchild'...\n")
213
+
214
+ expect(@db.models[:group].find(name: 'parent')).not_to be_nil
215
+ %w[child grandchild].each do |name|
216
+ expect(@db.models[:group].find(name: name)).to be_nil
217
+ end
218
+
219
+ host = @db.models[:host].find(name: 'child-host')
220
+ expect(host.groups_dataset[name: 'ungrouped']).not_to be_nil
221
+ end
222
+
223
+ #------------------------
224
+ it 'GROUP CHILDGROUP --delete-orphans ... should preserve child groups with another parent' do
225
+ runner { @app.start(%w[group add parent other-parent]) }
226
+ runner { @app.start(%w[group addchild parent child]) }
227
+ runner { @app.start(%w[group addchild other-parent child]) }
228
+
229
+ actual = runner do
230
+ @app.start(%w[group rmchild --delete-orphans parent child --yes])
231
+ end
232
+
233
+ expect(actual[:unexpected]).to eq(false)
234
+ expect(actual[:aborted]).to eq(false)
235
+
236
+ child = @db.models[:group].find(name: 'child')
237
+ expect(child).not_to be_nil
238
+ expect(child.parents_dataset[name: 'parent']).to be_nil
239
+ expect(child.parents_dataset[name: 'other-parent']).not_to be_nil
240
+ end
172
241
  end
173
242
  end
243
+ # rubocop:enable Metrics/BlockLength