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,44 +1,28 @@
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
- @console = Moose::Inventory::Cli::Formatter
22
- @config = Moose::Inventory::Config
23
- @config.init(@mockargs)
24
-
25
- @db = Moose::Inventory::DB
26
- @db.init if @db.db.nil?
27
-
28
- @group = Moose::Inventory::Cli::Group
29
- @host = Moose::Inventory::Cli::Host
30
- @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
+ )
31
15
  end
32
16
 
33
17
  before(:each) do
34
- @db.reset
18
+ reset_cli_harness
35
19
  end
36
20
 
37
21
  #====================
38
22
  describe 'rmgroup' do
39
23
  #----------------
40
24
  it 'should be responsive' do
41
- result = @group.instance_methods(false).include?(:rmhost)
25
+ result = @group.method_defined?(:rmhost, false)
42
26
  expect(result).to eq(true)
43
27
  end
44
28
 
@@ -47,7 +31,7 @@ RSpec.describe Moose::Inventory::Cli::Group do
47
31
  #------------------------
48
32
  it '<missing args> ... should abort with an error' do
49
33
  actual = runner do
50
- @app.start(%w(group rmhost)) # <- no group or hosts given
34
+ @app.start(%w[group rmhost]) # <- no group or hosts given
51
35
  end
52
36
 
53
37
  # Check output
@@ -61,16 +45,16 @@ RSpec.describe Moose::Inventory::Cli::Group do
61
45
  group_name = 'not-a-group'
62
46
  host_name = 'example'
63
47
  actual = runner do
64
- @app.start(%W(group rmhost #{group_name} #{host_name}))
48
+ @app.start(%W[group rmhost #{group_name} #{host_name} --yes])
65
49
  end
66
50
 
67
51
  # Check output
68
52
  desired = { aborted: true }
69
53
  desired[:STDOUT] =
70
- "Dissociate group '#{group_name}' from host(s) '#{host_name}':\n"\
71
- " - retrieve group '#{group_name}'...\n"
54
+ "Dissociate group '#{group_name}' from host(s) '#{host_name}':\n " \
55
+ "- retrieve group '#{group_name}'...\n"
72
56
  desired[:STDERR] =
73
- "ERROR: The group '#{group_name}' does not exist.\n"\
57
+ "ERROR: The group '#{group_name}' does not exist.\n" \
74
58
  "An error occurred during a transaction, any changes have been rolled back.\n"
75
59
  expected(actual, desired)
76
60
  end
@@ -80,9 +64,9 @@ RSpec.describe Moose::Inventory::Cli::Group do
80
64
  host_name = 'test1'
81
65
  group_name = 'group1'
82
66
 
83
- runner { @app.start(%W(host add #{host_name})) }
84
- runner { @app.start(%W(group add #{group_name})) }
85
- runner { @app.start(%W(group addhost #{group_name} #{host_name})) }
67
+ runner { @app.start(%W[host add #{host_name}]) }
68
+ runner { @app.start(%W[group add #{group_name}]) }
69
+ runner { @app.start(%W[group addhost #{group_name} #{host_name}]) }
86
70
 
87
71
  #
88
72
  # Dissociate the host
@@ -90,32 +74,48 @@ RSpec.describe Moose::Inventory::Cli::Group do
90
74
  # 2. expect that no association with ungrouped is made.
91
75
 
92
76
  actual = runner do
93
- @app.start(%W(group rmhost #{group_name} #{host_name}))
77
+ @app.start(%W[group rmhost #{group_name} #{host_name} --yes])
94
78
  end
95
79
 
96
80
  # @console.dump(actual, 'y')
97
81
 
98
- # rubocop:disable Metrics/LineLength
99
82
  desired = { aborted: false }
100
83
  desired[:STDOUT] =
101
- "Dissociate group '#{group_name}' from host(s) '#{host_name}':\n"\
102
- " - retrieve group '#{group_name}'...\n"\
103
- " - OK\n"\
104
- " - remove association {group:#{group_name} <-> host:#{host_name}}...\n"\
105
- " - OK\n"\
106
- " - add automatic association {group:ungrouped <-> host:#{host_name}}...\n"\
107
- " - OK\n"\
108
- " - all OK\n"\
84
+ "Dissociate group '#{group_name}' from host(s) '#{host_name}':\n " \
85
+ "- retrieve group '#{group_name}'...\n " \
86
+ "- OK\n " \
87
+ "- remove association {group:#{group_name} <-> host:#{host_name}}...\n " \
88
+ "- OK\n " \
89
+ "- add automatic association {group:ungrouped <-> host:#{host_name}}...\n " \
90
+ "- OK\n " \
91
+ "- all OK\n" \
109
92
  "Succeeded.\n"
110
93
  expected(actual, desired)
111
- # rubocop:enable Metrics/LineLength
112
-
113
94
  # We should have the correct group associations
114
95
  group = @db.models[:group].find(name: group_name)
115
96
  hosts = group.hosts_dataset
116
97
  expect(hosts.count).to eq(0)
117
98
  end
118
99
 
100
+ #------------------------
101
+ it 'group rmhost GROUP HOST --dry-run should not remove membership or add ungrouped' do
102
+ host_name = 'test1'
103
+ group_name = 'group1'
104
+ runner { @app.start(%W[host add #{host_name}]) }
105
+ runner { @app.start(%W[group add #{group_name}]) }
106
+ runner { @app.start(%W[group addhost #{group_name} #{host_name}]) }
107
+
108
+ actual = runner { @app.start(%W[group rmhost #{group_name} #{host_name} --dry-run]) }
109
+
110
+ expect(actual[:unexpected]).to eq(false)
111
+ expect(actual[:aborted]).to eq(false)
112
+ expect(actual[:STDOUT]).to include('Dry run complete. No changes applied.')
113
+ group = @db.models[:group].find(name: group_name)
114
+ host = @db.models[:host].find(name: host_name)
115
+ expect(group.hosts_dataset[name: host_name]).not_to be_nil
116
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
117
+ end
118
+
119
119
  #------------------------
120
120
  it 'GROUP HOST ... should warn about non-existing associations' do
121
121
  # 1. Should warn that the association doesn't exist.
@@ -123,28 +123,27 @@ RSpec.describe Moose::Inventory::Cli::Group do
123
123
 
124
124
  host_name = 'test_host'
125
125
  group_name = 'test_group'
126
- runner { @app.start(%W(host add #{host_name})) }
127
- runner { @app.start(%W(group add #{group_name})) }
128
- runner { @app.start(%W(group addhost #{host_name})) }
126
+ runner { @app.start(%W[host add #{host_name}]) }
127
+ runner { @app.start(%W[group add #{group_name}]) }
128
+ runner { @app.start(%W[group addhost #{host_name}]) }
129
129
 
130
130
  actual = runner do
131
- @app.start(%W(group rmhost #{group_name} #{host_name}))
131
+ @app.start(%W[group rmhost #{group_name} #{host_name} --yes])
132
132
  end
133
133
 
134
- # rubocop:disable Metrics/LineLength
135
134
  desired = { aborted: false }
136
135
  desired[:STDOUT] =
137
- "Dissociate group '#{group_name}' from host(s) '#{host_name}':\n"\
138
- " - retrieve group \'#{group_name}\'...\n"\
139
- " - OK\n"\
140
- " - remove association {group:#{group_name} <-> host:#{host_name}}...\n"\
141
- " - doesn't exist, skipping.\n"\
142
- " - OK\n"\
143
- " - all OK\n"\
136
+ "Dissociate group '#{group_name}' from host(s) '#{host_name}':\n " \
137
+ "- retrieve group '#{group_name}'...\n " \
138
+ "- OK\n " \
139
+ "- remove association {group:#{group_name} <-> host:#{host_name}}...\n " \
140
+ "- doesn't exist, skipping.\n " \
141
+ "- OK\n " \
142
+ "- all OK\n" \
144
143
  "Succeeded, with warnings.\n"
145
144
  desired[:STDERR] =
146
- "WARNING: Association {group:#{group_name} <-> host:#{host_name}} "\
147
- "doesn't exist, skipping.\n"
145
+ "WARNING: Association {group:#{group_name} <-> host:#{host_name}} " \
146
+ "doesn't exist, skipping.\n"
148
147
 
149
148
  expected(actual, desired)
150
149
  end
@@ -154,9 +153,9 @@ RSpec.describe Moose::Inventory::Cli::Group do
154
153
  host_name = 'test_host'
155
154
  group_name = 'ungrouped'
156
155
 
157
- runner { @app.start(%W(host add #{name})) } # <- auto creates the association with ungrouped
156
+ runner { @app.start(%W[host add #{name}]) } # <- auto creates the association with ungrouped
158
157
 
159
- actual = runner { @app.start(%W(group rmhost #{group_name} #{host_name})) }
158
+ actual = runner { @app.start(%W[group rmhost #{group_name} #{host_name} --yes]) }
160
159
 
161
160
  desired = { aborted: true }
162
161
  desired[:STDERR] =
@@ -165,41 +164,39 @@ RSpec.describe Moose::Inventory::Cli::Group do
165
164
  end
166
165
 
167
166
  #------------------------
168
- it 'GROUP HOST1 HOST2 ... should dissociate the group from'\
169
- ' multiple hosts at once' do
167
+ it 'GROUP HOST1 HOST2 ... should dissociate the group from ' \
168
+ 'multiple hosts at once' do
170
169
  # 1. Should dissociate hosts from the group
171
170
  # 2. Should add each host to the 'ungrouped' automatic group
172
171
  # if it has no other groups.
173
172
 
174
173
  group_name = 'test_group'
175
- host_names = %w(test_host1 test_host2 test_host3)
174
+ host_names = %w[test_host1 test_host2 test_host3]
176
175
 
177
- runner { @app.start(%W(group add #{group_name})) }
178
- runner { @app.start(%W(group addhost #{group_name}) + host_names) }
176
+ runner { @app.start(%W[group add #{group_name}]) }
177
+ runner { @app.start(%W[group addhost #{group_name}] + host_names) }
179
178
 
180
179
  actual = runner do
181
- @app.start(%W(group rmhost #{group_name}) + host_names)
180
+ @app.start(%W[group rmhost #{group_name} --yes] + host_names)
182
181
  end
183
182
 
184
183
  # @console.out(actual, 'y')
185
184
  desired = { aborted: false }
186
185
  desired[:STDOUT] =
187
- "Dissociate group '#{group_name}' from host(s) '#{host_names.join(',')}':\n"\
188
- " - retrieve group \'#{group_name}\'...\n"\
189
- " - OK\n"
186
+ "Dissociate group '#{group_name}' from host(s) '#{host_names.join(',')}':\n " \
187
+ "- retrieve group '#{group_name}'...\n " \
188
+ "- OK\n"
190
189
  host_names.each do |host|
191
190
  desired[:STDOUT] = desired[:STDOUT] +
192
- " - remove association {group:#{group_name} <-> host:#{host}}...\n"\
193
- " - OK\n"\
194
- " - add automatic association {group:ungrouped <-> host:#{host}}...\n"\
195
- " - OK\n"\
191
+ " - remove association {group:#{group_name} <-> host:#{host}}...\n " \
192
+ "- OK\n " \
193
+ "- add automatic association {group:ungrouped <-> host:#{host}}...\n " \
194
+ "- OK\n" \
196
195
  end
197
196
  desired[:STDOUT] = desired[:STDOUT] +
198
- " - all OK\n"\
197
+ " - all OK\n" \
199
198
  "Succeeded.\n"
200
199
  expected(actual, desired)
201
- # rubocop:enable Metrics/LineLength
202
-
203
200
  # We should have the correct group associations
204
201
  group = @db.models[:group].find(name: group_name)
205
202
  hosts = group.hosts_dataset
@@ -210,3 +207,4 @@ RSpec.describe Moose::Inventory::Cli::Group do
210
207
  end
211
208
  end
212
209
  end
210
+ # rubocop:enable Metrics/BlockLength
@@ -1,48 +1,29 @@
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
- @db = Moose::Inventory::DB
25
- @db.init if @db.db.nil?
26
-
27
- @console = Moose::Inventory::Cli::Formatter
28
- @group = Moose::Inventory::Cli::Group
29
- @app = Moose::Inventory::Cli::Application
10
+ setup_cli_harness(command_class: Moose::Inventory::Cli::Group, command_ivar: :@group)
30
11
  end
31
12
 
32
13
  before(:each) do
33
- @db.reset
14
+ reset_cli_harness
34
15
  end
35
16
 
36
17
  describe 'rmvar' do
37
18
  it 'should be responsive' do
38
- result = @group.instance_methods(false).include?(:rmvar)
19
+ result = @group.method_defined?(:rmvar, false)
39
20
  expect(result).to eq(true)
40
21
  end
41
22
 
42
23
  #-----------------
43
24
  it '<missing args> ... should abort with an error' do
44
25
  actual = runner do
45
- @app.start(%w(group rmvar))
26
+ @app.start(%w[group rmvar])
46
27
  end
47
28
 
48
29
  # Check output
@@ -56,16 +37,16 @@ RSpec.describe Moose::Inventory::Cli::Group do
56
37
  group_name = 'does-not-exist'
57
38
  var_name = 'foo=bar'
58
39
  actual = runner do
59
- @app.start(%W(group rmvar #{group_name} #{var_name}))
40
+ @app.start(%W[group rmvar #{group_name} #{var_name} --yes])
60
41
  end
61
42
 
62
43
  # Check output
63
44
  desired = { aborted: true }
64
45
  desired[:STDOUT] =
65
- "Remove variable(s) '#{var_name}' from group '#{group_name}':\n"\
66
- " - retrieve group '#{group_name}'...\n"
46
+ "Remove variable(s) '#{var_name}' from group '#{group_name}':\n " \
47
+ "- retrieve group '#{group_name}'...\n"
67
48
  desired[:STDERR] =
68
- "An error occurred during a transaction, any changes have been rolled back.\n"\
49
+ "An error occurred during a transaction, any changes have been rolled back.\n" \
69
50
  "ERROR: The group '#{group_name}' does not exist.\n"
70
51
  expected(actual, desired)
71
52
  end
@@ -77,44 +58,57 @@ RSpec.describe Moose::Inventory::Cli::Group do
77
58
 
78
59
  group_name = 'test1'
79
60
  @db.models[:group].create(name: group_name)
80
-
81
- var = { name: 'foo', value: 'bar' }
82
- cases = %w(
61
+ cases = %w[
83
62
  =bar
84
63
  foo=bar=
85
64
  =foo=bar
86
65
  foo=bar=extra
87
- )
66
+ ]
88
67
 
89
68
  cases.each do |args|
90
69
  actual = runner do
91
- @app.start(%W(group rmvar #{group_name} #{args}))
70
+ @app.start(%W[group rmvar #{group_name} #{args} --yes])
92
71
  end
93
72
  # @console.out(actual,'p')
94
73
 
95
74
  desired = { aborted: true }
96
75
  desired[:STDOUT] =
97
- "Remove variable(s) '#{args}' from group '#{group_name}':\n"\
98
- " - retrieve group '#{group_name}'...\n"\
99
- " - OK\n"\
100
- " - remove variable '#{args}'...\n"
76
+ "Remove variable(s) '#{args}' from group '#{group_name}':\n " \
77
+ "- retrieve group '#{group_name}'...\n " \
78
+ "- OK\n " \
79
+ "- remove variable '#{args}'...\n"
101
80
  desired[:STDERR] =
102
- "An error occurred during a transaction, any changes have been rolled back.\n"\
81
+ "An error occurred during a transaction, any changes have been rolled back.\n" \
103
82
  "ERROR: Incorrect format in {#{args}}. Expected 'key' or 'key=value'.\n"
104
83
 
105
84
  expected(actual, desired)
106
85
  end
107
86
  end
108
87
 
88
+ #------------------------
89
+ it 'GROUP key --dry-run should not remove the group variable' do
90
+ group_name = 'group_test'
91
+ @db.models[:group].create(name: group_name)
92
+ runner { @app.start(%W[group addvar #{group_name} var1=val1]) }
93
+
94
+ actual = runner { @app.start(%W[group rmvar #{group_name} var1 --dry-run]) }
95
+
96
+ expect(actual[:unexpected]).to eq(false)
97
+ expect(actual[:aborted]).to eq(false)
98
+ expect(actual[:STDOUT]).to include('Dry run complete. No changes applied.')
99
+ group = @db.models[:group].find(name: group_name)
100
+ expect(group.groupvars_dataset[name: 'var1']).not_to be_nil
101
+ end
102
+
109
103
  #------------------------
110
104
  it 'GROUP <valid args> ... should remove the group variable' do
111
105
  group_name = 'group_test'
112
106
  var = { name: 'foo', value: 'bar' }
113
- cases = %W(
107
+ cases = %W[
114
108
  #{var[:name]}
115
109
  #{var[:name]}=
116
110
  #{var[:name]}=#{var[:value]}
117
- )
111
+ ]
118
112
  cases.each do |example|
119
113
  # reset the db
120
114
  @db.reset
@@ -122,24 +116,24 @@ RSpec.describe Moose::Inventory::Cli::Group do
122
116
  # Add an initial group and groupvar
123
117
  @db.models[:group].create(name: group_name)
124
118
  runner do
125
- @app.start(%W(group addvar #{group_name} #{var[:name]}=#{var[:value]}))
119
+ @app.start(%W[group addvar #{group_name} #{var[:name]}=#{var[:value]}])
126
120
  end
127
121
 
128
122
  # Try to remove the groupvar using the case example valid args
129
123
  actual = runner do
130
- @app.start(%W(group rmvar #{group_name} #{example}))
124
+ @app.start(%W[group rmvar #{group_name} #{example} --yes])
131
125
  end
132
126
  # @console.out(actual,'p')
133
127
 
134
128
  # Check the output
135
129
  desired = { aborted: false }
136
130
  desired[:STDOUT] =
137
- "Remove variable(s) '#{example}' from group '#{group_name}':\n"\
138
- " - retrieve group '#{group_name}'...\n"\
139
- " - OK\n"\
140
- " - remove variable '#{example}'...\n"\
141
- " - OK\n"\
142
- " - all OK\n"\
131
+ "Remove variable(s) '#{example}' from group '#{group_name}':\n " \
132
+ "- retrieve group '#{group_name}'...\n " \
133
+ "- OK\n " \
134
+ "- remove variable '#{example}'...\n " \
135
+ "- OK\n " \
136
+ "- all OK\n" \
143
137
  "Succeeded.\n"
144
138
 
145
139
  # @console.out(desired,'p')
@@ -160,36 +154,35 @@ RSpec.describe Moose::Inventory::Cli::Group do
160
154
  group_name = 'test_group'
161
155
  varsarray = [
162
156
  { name: 'var1', value: 'val1' },
163
- { name: 'var2', value: 'val2' },
157
+ { name: 'var2', value: 'val2' }
164
158
  ]
165
159
 
166
- vars = []
167
- varsarray.each do |var|
168
- vars << "#{var[:name]}=#{var[:value]}"
160
+ vars = varsarray.map do |var|
161
+ "#{var[:name]}=#{var[:value]}"
169
162
  end
170
163
 
171
164
  @db.models[:group].create(name: group_name)
172
- actual = runner do
173
- @app.start(%W(group addvar #{group_name}) + vars)
165
+ runner do
166
+ @app.start(%W[group addvar #{group_name}] + vars)
174
167
  end
175
168
 
176
169
  actual = runner do
177
- @app.start(%W(group rmvar #{group_name}) + vars)
170
+ @app.start(%W[group rmvar #{group_name} --yes] + vars)
178
171
  end
179
172
  # @console.out(actual,'y')
180
173
 
181
174
  desired = { aborted: false }
182
175
  desired[:STDOUT] =
183
- "Remove variable(s) '#{vars.join(',')}' from group '#{group_name}':\n"\
184
- " - retrieve group '#{group_name}'...\n"\
185
- " - OK\n"
176
+ "Remove variable(s) '#{vars.join(',')}' from group '#{group_name}':\n " \
177
+ "- retrieve group '#{group_name}'...\n " \
178
+ "- OK\n"
186
179
  vars.each do |var|
187
180
  desired[:STDOUT] = desired[:STDOUT] +
188
- " - remove variable '#{var}'...\n"\
189
- " - OK\n"
181
+ " - remove variable '#{var}'...\n " \
182
+ "- OK\n"
190
183
  end
191
184
  desired[:STDOUT] = desired[:STDOUT] +
192
- " - all OK\n"\
185
+ " - all OK\n" \
193
186
  "Succeeded.\n"
194
187
  expected(actual, desired)
195
188
 
@@ -200,3 +193,4 @@ RSpec.describe Moose::Inventory::Cli::Group do
200
193
  end
201
194
  end
202
195
  end
196
+ # rubocop:enable Metrics/BlockLength
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  require_relative 'group_add_spec'
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # rubocop:disable Metrics/BlockLength
6
+ RSpec.describe Moose::Inventory::Cli::Helpers do
7
+ subject(:helper) { helper_class.new }
8
+
9
+ let(:helper_class) do
10
+ Class.new do
11
+ include Moose::Inventory::Cli::Helpers
12
+ end
13
+ end
14
+
15
+ let(:inventory_context) { instance_double(Moose::Inventory::InventoryContext) }
16
+ let(:automatic_group) { instance_double('AutomaticGroup') }
17
+
18
+ before do
19
+ helper.instance_variable_set(:@inventory_context, inventory_context)
20
+ allow(inventory_context).to receive(:automatic_group).and_return(automatic_group)
21
+ end
22
+
23
+ describe 'small helper methods' do
24
+ it 'checks whether an association dataset contains a named record' do
25
+ dataset = instance_double('Dataset')
26
+
27
+ expect(helper.send(:association_exists?, nil, 'alpha')).to eq(false)
28
+ allow(dataset).to receive(:[]).with(name: 'alpha').and_return(nil)
29
+ expect(helper.send(:association_exists?, dataset, 'alpha')).to eq(false)
30
+ allow(dataset).to receive(:[]).with(name: 'beta').and_return({ name: 'beta' })
31
+ expect(helper.send(:association_exists?, dataset, 'beta')).to eq(true)
32
+ end
33
+
34
+ it 'converts exceptions to strings' do
35
+ expect(helper.send(:exception_to_s, RuntimeError.new('boom'))).to eq('boom')
36
+ end
37
+
38
+ it 'returns the automatic group through the inventory context' do
39
+ expect(helper.send(:automatic_group)).to eq(automatic_group)
40
+ end
41
+ end
42
+
43
+ describe 'run_group_relation_transaction' do
44
+ it 'prints the heading and success marker for a successful transaction' do
45
+ allow(Moose::Inventory::DB).to receive(:transaction).and_yield
46
+ expect(Moose::Inventory::Cli::Formatter).to receive(:puts).with(2, '- all OK')
47
+
48
+ result = nil
49
+ actual = runner do
50
+ result = helper.send(:run_group_relation_transaction, heading: 'Heading') { :done }
51
+ end
52
+
53
+ expect(result).to eq(:done)
54
+ expected(actual, aborted: false, STDOUT: "Heading\n", STDERR: '')
55
+ end
56
+
57
+ it 'aborts with the Moose exception message when no custom handler is supplied' do
58
+ error = Moose::Inventory::DB::MooseDBException.new('boom')
59
+ allow(Moose::Inventory::DB).to receive(:transaction).and_raise(error)
60
+ allow(Moose::Inventory::DB).to receive(:exceptions).and_return({ moose: Moose::Inventory::DB::MooseDBException })
61
+
62
+ actual = runner do
63
+ helper.send(:run_group_relation_transaction, heading: 'Heading') { :done }
64
+ end
65
+
66
+ expected(actual, aborted: true, STDOUT: '', STDERR: "ERROR: boom\n")
67
+ end
68
+
69
+ it 'uses the custom Moose exception handler when provided' do
70
+ error = Moose::Inventory::DB::MooseDBException.new('boom')
71
+ allow(Moose::Inventory::DB).to receive(:transaction).and_raise(error)
72
+ allow(Moose::Inventory::DB).to receive(:exceptions).and_return({ moose: Moose::Inventory::DB::MooseDBException })
73
+
74
+ actual = runner do
75
+ helper.send(
76
+ :run_group_relation_transaction,
77
+ heading: 'Heading',
78
+ on_error: ->(e) { "wrapped #{e.message}" }
79
+ ) { :done }
80
+ end
81
+
82
+ expected(actual, aborted: true, STDOUT: '', STDERR: "ERROR: wrapped boom\n")
83
+ end
84
+ end
85
+
86
+ describe 'automatic-group helpers' do
87
+ it 'removes the automatic group from a host when present' do
88
+ host = instance_double('Host')
89
+ dataset = instance_double('Dataset')
90
+ ungrouped = instance_double('Group')
91
+ allow(host).to receive(:groups_dataset).and_return(dataset)
92
+ allow(dataset).to receive(:[]).with(name: 'ungrouped').and_return(ungrouped)
93
+ expect(Moose::Inventory::Cli::Formatter).to receive(:puts).with(2, 'remove auto')
94
+ expect(host).to receive(:remove_group).with(ungrouped)
95
+ expect(Moose::Inventory::Cli::Formatter).to receive(:puts).with(4, '- OK')
96
+
97
+ helper.send(:remove_automatic_group_from_host, host, indent: 2, message: 'remove auto')
98
+ end
99
+
100
+ it 'does nothing when the automatic group is not present' do
101
+ host = instance_double('Host')
102
+ dataset = instance_double('Dataset')
103
+ allow(host).to receive(:groups_dataset).and_return(dataset)
104
+ allow(dataset).to receive(:[]).with(name: 'ungrouped').and_return(nil)
105
+
106
+ expect(Moose::Inventory::Cli::Formatter).not_to receive(:puts)
107
+ expect(host).not_to receive(:remove_group)
108
+
109
+ helper.send(:remove_automatic_group_from_host, host, indent: 2, message: 'remove auto')
110
+ end
111
+
112
+ it 'adds the automatic group when the last non-automatic group is removed' do
113
+ host = instance_double('Host')
114
+ dataset = instance_double('Dataset', count: 1)
115
+ allow(host).to receive(:groups_dataset).and_return(dataset)
116
+ expect(Moose::Inventory::Cli::Formatter).to receive(:puts).with(2, 'add auto')
117
+ expect(host).to receive(:add_group).with(automatic_group)
118
+ expect(Moose::Inventory::Cli::Formatter).to receive(:puts).with(4, '- OK')
119
+
120
+ helper.send(:add_automatic_group_to_host_if_last_group, host, indent: 2, message: 'add auto')
121
+ end
122
+
123
+ it 'adds the automatic group when a host has no groups' do
124
+ host = instance_double('Host')
125
+ dataset = instance_double('Dataset', count: 0)
126
+ allow(host).to receive(:groups_dataset).and_return(dataset)
127
+ expect(Moose::Inventory::Cli::Formatter).to receive(:puts).with(2, 'add auto')
128
+ expect(host).to receive(:add_group).with(automatic_group)
129
+ expect(Moose::Inventory::Cli::Formatter).to receive(:puts).with(4, '- OK')
130
+
131
+ helper.send(:add_automatic_group_to_host_if_no_groups, host, indent: 2, message: 'add auto')
132
+ end
133
+
134
+ it 'does nothing when the group count does not match the requested threshold' do
135
+ host = instance_double('Host')
136
+ dataset = instance_double('Dataset', count: 2)
137
+ allow(host).to receive(:groups_dataset).and_return(dataset)
138
+
139
+ expect(Moose::Inventory::Cli::Formatter).not_to receive(:puts)
140
+ expect(host).not_to receive(:add_group)
141
+
142
+ helper.send(:add_automatic_group_to_host_if_group_count, host, 1, indent: 2, message: 'add auto')
143
+ end
144
+ end
145
+ end
146
+ # rubocop:enable Metrics/BlockLength