moose-inventory 2.0 → 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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +2 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +21 -0
  5. data/BACKLOG.md +630 -8
  6. data/Gemfile +2 -0
  7. data/Gemfile.lock +1 -1
  8. data/README.md +315 -39
  9. data/Rakefile +2 -0
  10. data/bin/moose-inventory +2 -1
  11. data/docs/architecture/architecture-and-trust-boundaries.md +444 -0
  12. data/docs/compatibility/cli-output-compatibility.md +76 -0
  13. data/docs/governance/approval-register.md +37 -0
  14. data/docs/maintenance/database-backup-restore-guidance.md +162 -0
  15. data/docs/maintenance/package-maintenance-and-agent-boundaries.md +260 -0
  16. data/docs/process/conformance-gap-analysis-2026-05-28.md +192 -0
  17. data/docs/product/product-brief.md +161 -0
  18. data/docs/product/requirements-baseline.md +477 -0
  19. data/docs/qa/qa-documentation-and-release-gates.md +283 -0
  20. data/docs/release/package-provenance-hardening.md +126 -0
  21. data/docs/release/publishing.md +11 -3
  22. data/docs/release/release-environment-protection.md +70 -0
  23. data/docs/release/release-readiness.md +23 -4
  24. data/docs/security/accepted-risk-register.md +84 -0
  25. data/docs/security/security-privacy-process.md +287 -0
  26. data/docs/security-audit-2026-05-26-rerun.md +2 -2
  27. data/docs/ux/cli-workflow-notes.md +287 -0
  28. data/examples/ansible/ansible.cfg +3 -0
  29. data/examples/ansible/inventory/moose_inventory.yml +5 -0
  30. data/examples/ansible/inventory_plugins/moose_inventory.py +100 -0
  31. data/examples/ci/README.md +16 -0
  32. data/examples/ci/github-actions/inventory-review.yml +38 -0
  33. data/examples/ci/inventory/example-snapshot.yml +19 -0
  34. data/examples/ci/scripts/validate-inventory-snapshot.sh +30 -0
  35. data/lib/moose_inventory/cli/application.rb +133 -5
  36. data/lib/moose_inventory/cli/association_rendering.rb +74 -0
  37. data/lib/moose_inventory/cli/association_rendering_support.rb +89 -0
  38. data/lib/moose_inventory/cli/audit.rb +62 -0
  39. data/lib/moose_inventory/cli/audit_recording.rb +40 -0
  40. data/lib/moose_inventory/cli/child_relation_rendering.rb +110 -0
  41. data/lib/moose_inventory/cli/console.rb +135 -0
  42. data/lib/moose_inventory/cli/db.rb +64 -0
  43. data/lib/moose_inventory/cli/factory.rb +28 -0
  44. data/lib/moose_inventory/cli/formatter.rb +8 -12
  45. data/lib/moose_inventory/cli/group.rb +5 -2
  46. data/lib/moose_inventory/cli/group_add.rb +11 -9
  47. data/lib/moose_inventory/cli/group_addchild.rb +23 -65
  48. data/lib/moose_inventory/cli/group_addhost.rb +16 -67
  49. data/lib/moose_inventory/cli/group_addvar.rb +27 -47
  50. data/lib/moose_inventory/cli/group_get.rb +8 -42
  51. data/lib/moose_inventory/cli/group_list.rb +7 -40
  52. data/lib/moose_inventory/cli/group_listvars.rb +9 -55
  53. data/lib/moose_inventory/cli/group_rm.rb +12 -10
  54. data/lib/moose_inventory/cli/group_rmchild.rb +26 -82
  55. data/lib/moose_inventory/cli/group_rmhost.rb +18 -53
  56. data/lib/moose_inventory/cli/group_rmvar.rb +30 -41
  57. data/lib/moose_inventory/cli/group_tags.rb +33 -0
  58. data/lib/moose_inventory/cli/helpers.rb +68 -1
  59. data/lib/moose_inventory/cli/host.rb +6 -3
  60. data/lib/moose_inventory/cli/host_add.rb +69 -29
  61. data/lib/moose_inventory/cli/host_addgroup.rb +22 -58
  62. data/lib/moose_inventory/cli/host_addvar.rb +28 -52
  63. data/lib/moose_inventory/cli/host_get.rb +9 -37
  64. data/lib/moose_inventory/cli/host_list.rb +24 -21
  65. data/lib/moose_inventory/cli/host_listvars.rb +9 -62
  66. data/lib/moose_inventory/cli/host_rm.rb +60 -42
  67. data/lib/moose_inventory/cli/host_rmgroup.rb +25 -44
  68. data/lib/moose_inventory/cli/host_rmvar.rb +31 -45
  69. data/lib/moose_inventory/cli/host_tags.rb +33 -0
  70. data/lib/moose_inventory/cli/listvars_support.rb +55 -0
  71. data/lib/moose_inventory/cli/plan_rendering.rb +50 -0
  72. data/lib/moose_inventory/cli/relation_transaction_support.rb +51 -0
  73. data/lib/moose_inventory/cli/tag_support.rb +97 -0
  74. data/lib/moose_inventory/cli/variable_rendering.rb +67 -0
  75. data/lib/moose_inventory/config/config.rb +185 -108
  76. data/lib/moose_inventory/db/db.rb +170 -195
  77. data/lib/moose_inventory/db/exceptions.rb +6 -3
  78. data/lib/moose_inventory/db/models.rb +16 -0
  79. data/lib/moose_inventory/db/schema_migrations.rb +248 -0
  80. data/lib/moose_inventory/inventory_context.rb +68 -2
  81. data/lib/moose_inventory/operations/add_associations.rb +20 -16
  82. data/lib/moose_inventory/operations/add_groups.rb +21 -13
  83. data/lib/moose_inventory/operations/add_hosts.rb +30 -17
  84. data/lib/moose_inventory/operations/add_variables.rb +77 -0
  85. data/lib/moose_inventory/operations/entity_variable_operation_support.rb +46 -0
  86. data/lib/moose_inventory/operations/group_child_relations.rb +23 -16
  87. data/lib/moose_inventory/operations/group_cleanup.rb +23 -8
  88. data/lib/moose_inventory/operations/import_inventory_snapshot.rb +41 -0
  89. data/lib/moose_inventory/operations/inventory_doctor.rb +172 -0
  90. data/lib/moose_inventory/operations/inventory_snapshot.rb +60 -0
  91. data/lib/moose_inventory/operations/inventory_snapshot_applier.rb +112 -0
  92. data/lib/moose_inventory/operations/inventory_snapshot_preview.rb +174 -0
  93. data/lib/moose_inventory/operations/inventory_snapshot_validator.rb +134 -0
  94. data/lib/moose_inventory/operations/operation_event_support.rb +27 -0
  95. data/lib/moose_inventory/operations/query_inventory/base_query.rb +24 -0
  96. data/lib/moose_inventory/operations/query_inventory/group_queries.rb +86 -0
  97. data/lib/moose_inventory/operations/query_inventory/host_queries.rb +106 -0
  98. data/lib/moose_inventory/operations/query_inventory.rb +47 -0
  99. data/lib/moose_inventory/operations/remove_associations.rb +30 -18
  100. data/lib/moose_inventory/operations/remove_groups.rb +12 -12
  101. data/lib/moose_inventory/operations/remove_hosts.rb +68 -0
  102. data/lib/moose_inventory/operations/remove_variables.rb +67 -0
  103. data/lib/moose_inventory/runtime_options.rb +31 -0
  104. data/lib/moose_inventory/version.rb +3 -1
  105. data/lib/moose_inventory.rb +10 -7
  106. data/moose-inventory.gemspec +19 -35
  107. data/scripts/check.sh +1 -0
  108. data/scripts/ci/check_generated_artifacts.sh +41 -0
  109. data/scripts/ci/check_permissions.sh +2 -0
  110. data/scripts/ci/check_rubocop.sh +30 -25
  111. data/scripts/files.rb +5 -4
  112. data/spec/examples/ci_examples_spec.rb +37 -0
  113. data/spec/lib/moose_inventory/ansible_plugin_examples_spec.rb +29 -0
  114. data/spec/lib/moose_inventory/cli/application_doctor_spec.rb +50 -0
  115. data/spec/lib/moose_inventory/cli/application_import_export_spec.rb +100 -0
  116. data/spec/lib/moose_inventory/cli/application_spec.rb +25 -15
  117. data/spec/lib/moose_inventory/cli/audit_spec.rb +56 -0
  118. data/spec/lib/moose_inventory/cli/cli_spec.rb +15 -19
  119. data/spec/lib/moose_inventory/cli/console_spec.rb +98 -0
  120. data/spec/lib/moose_inventory/cli/factory_spec.rb +27 -0
  121. data/spec/lib/moose_inventory/cli/formatter_spec.rb +95 -3
  122. data/spec/lib/moose_inventory/cli/group_add_spec.rb +140 -116
  123. data/spec/lib/moose_inventory/cli/group_addchild_spec.rb +89 -35
  124. data/spec/lib/moose_inventory/cli/group_addhost_spec.rb +81 -84
  125. data/spec/lib/moose_inventory/cli/group_addvar_spec.rb +65 -68
  126. data/spec/lib/moose_inventory/cli/group_get_spec.rb +17 -33
  127. data/spec/lib/moose_inventory/cli/group_list_spec.rb +16 -38
  128. data/spec/lib/moose_inventory/cli/group_listvar_spec.rb +33 -40
  129. data/spec/lib/moose_inventory/cli/group_rm_spec.rb +136 -96
  130. data/spec/lib/moose_inventory/cli/group_rmchild_spec.rb +66 -41
  131. data/spec/lib/moose_inventory/cli/group_rmhost_spec.rb +76 -78
  132. data/spec/lib/moose_inventory/cli/group_rmvar_spec.rb +57 -63
  133. data/spec/lib/moose_inventory/cli/group_spec.rb +2 -0
  134. data/spec/lib/moose_inventory/cli/helpers_spec.rb +146 -0
  135. data/spec/lib/moose_inventory/cli/host_add_spec.rb +170 -116
  136. data/spec/lib/moose_inventory/cli/host_addgroup_spec.rb +100 -83
  137. data/spec/lib/moose_inventory/cli/host_addvar_spec.rb +92 -74
  138. data/spec/lib/moose_inventory/cli/host_get_spec.rb +14 -33
  139. data/spec/lib/moose_inventory/cli/host_list_spec.rb +41 -33
  140. data/spec/lib/moose_inventory/cli/host_listvar_spec.rb +45 -53
  141. data/spec/lib/moose_inventory/cli/host_rm_spec.rb +66 -48
  142. data/spec/lib/moose_inventory/cli/host_rmgroup_spec.rb +73 -83
  143. data/spec/lib/moose_inventory/cli/host_rmvar_spec.rb +56 -63
  144. data/spec/lib/moose_inventory/cli/host_spec.rb +2 -0
  145. data/spec/lib/moose_inventory/cli/tags_spec.rb +81 -0
  146. data/spec/lib/moose_inventory/config/config_spec.rb +41 -3
  147. data/spec/lib/moose_inventory/db/db_spec.rb +396 -36
  148. data/spec/lib/moose_inventory/db/exceptions_spec.rb +18 -0
  149. data/spec/lib/moose_inventory/db/models_spec.rb +7 -3
  150. data/spec/lib/moose_inventory/db_lifecycle_spec.rb +73 -0
  151. data/spec/lib/moose_inventory/inventory_context_spec.rb +10 -0
  152. data/spec/lib/moose_inventory/operations/add_associations_spec.rb +34 -0
  153. data/spec/lib/moose_inventory/operations/add_groups_spec.rb +15 -0
  154. data/spec/lib/moose_inventory/operations/add_hosts_spec.rb +13 -0
  155. data/spec/lib/moose_inventory/operations/add_variables_spec.rb +103 -0
  156. data/spec/lib/moose_inventory/operations/group_child_relations_spec.rb +46 -0
  157. data/spec/lib/moose_inventory/operations/import_inventory_snapshot_spec.rb +226 -0
  158. data/spec/lib/moose_inventory/operations/inventory_doctor_spec.rb +77 -0
  159. data/spec/lib/moose_inventory/operations/inventory_snapshot_spec.rb +50 -0
  160. data/spec/lib/moose_inventory/operations/operation_event_support_spec.rb +78 -0
  161. data/spec/lib/moose_inventory/operations/query_inventory_spec.rb +146 -0
  162. data/spec/lib/moose_inventory/operations/remove_associations_spec.rb +35 -0
  163. data/spec/lib/moose_inventory/operations/remove_groups_spec.rb +21 -0
  164. data/spec/lib/moose_inventory/operations/remove_hosts_spec.rb +55 -0
  165. data/spec/lib/moose_inventory/operations/remove_variables_spec.rb +83 -0
  166. data/spec/shared/shared_config_setup.rb +4 -3
  167. data/spec/spec_helper.rb +50 -40
  168. data/spec/support/cli_harness.rb +33 -0
  169. metadata +80 -41
@@ -1,43 +1,24 @@
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::Host 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
- @host = Moose::Inventory::Cli::Host
29
- @app = Moose::Inventory::Cli::Application
10
+ setup_cli_harness(command_class: Moose::Inventory::Cli::Host, command_ivar: :@host)
30
11
  end
31
12
 
32
13
  before(:each) do
33
- @db.reset
14
+ reset_cli_harness
34
15
  end
35
16
 
36
17
  #====================
37
18
  describe 'rmgroup' do
38
19
  #----------------
39
20
  it 'should be responsive' do
40
- result = @host.instance_methods(false).include?(:rmgroup)
21
+ result = @host.method_defined?(:rmgroup, false)
41
22
  expect(result).to eq(true)
42
23
  end
43
24
 
@@ -46,7 +27,7 @@ RSpec.describe Moose::Inventory::Cli::Host do
46
27
  #------------------------
47
28
  it 'host rmgroup <missing args> ... should abort with an error' do
48
29
  actual = runner do
49
- @app.start(%w(host rmgroup)) # <- no group given
30
+ @app.start(%w[host rmgroup]) # <- no group given
50
31
  end
51
32
 
52
33
  # Check output
@@ -60,16 +41,16 @@ RSpec.describe Moose::Inventory::Cli::Host do
60
41
  host_name = 'not-a-host'
61
42
  group_name = 'example'
62
43
  actual = runner do
63
- @app.start(%W(host rmgroup #{host_name} #{group_name}))
44
+ @app.start(%W[host rmgroup #{host_name} #{group_name} --yes])
64
45
  end
65
46
 
66
47
  # Check output
67
48
  desired = { aborted: true }
68
49
  desired[:STDOUT] =
69
- "Dissociate host '#{host_name}' from groups '#{group_name}':\n"\
70
- " - Retrieve host '#{host_name}'...\n"
50
+ "Dissociate host '#{host_name}' from groups '#{group_name}':\n " \
51
+ "- Retrieve host '#{host_name}'...\n"
71
52
  desired[:STDERR] =
72
- "An error occurred during a transaction, any changes have been rolled back.\n"\
53
+ "An error occurred during a transaction, any changes have been rolled back.\n" \
73
54
  "ERROR: The host '#{host_name}' was not found in the database.\n"
74
55
  expected(actual, desired)
75
56
  end
@@ -81,10 +62,10 @@ RSpec.describe Moose::Inventory::Cli::Host do
81
62
  # if it has no other groups.
82
63
 
83
64
  host_name = 'test1'
84
- runner { @app.start(%W(host add #{host_name})) }
65
+ runner { @app.start(%W[host add #{host_name}]) }
85
66
 
86
- group_names = %w(group1 group2)
87
- tmp = runner { @app.start(%W(host addgroup #{host_name} #{group_names[0]} #{group_names[1]})) }
67
+ group_names = %w[group1 group2]
68
+ runner { @app.start(%W[host addgroup #{host_name} #{group_names[0]} #{group_names[1]}]) }
88
69
 
89
70
  #
90
71
  # Dissociate from the first group
@@ -92,23 +73,20 @@ RSpec.describe Moose::Inventory::Cli::Host do
92
73
  # 2. expect that no association with ungrouped is made.
93
74
 
94
75
  actual = runner do
95
- @app.start(%W(host rmgroup #{host_name} #{group_names[0]}))
76
+ @app.start(%W[host rmgroup #{host_name} #{group_names[0]} --yes])
96
77
  end
97
78
  # @console.dump(actual, 'y')
98
79
 
99
- # rubocop:disable Metrics/LineLength
100
80
  desired = { aborted: false }
101
81
  desired[:STDOUT] =
102
- "Dissociate host '#{host_name}' from groups '#{group_names[0]}':\n"\
103
- " - Retrieve host '#{host_name}'...\n"\
104
- " - OK\n"\
105
- " - Remove association {host:#{host_name} <-> group:#{group_names[0]}}...\n"\
106
- " - OK\n"\
107
- " - All OK\n"\
82
+ "Dissociate host '#{host_name}' from groups '#{group_names[0]}':\n " \
83
+ "- Retrieve host '#{host_name}'...\n " \
84
+ "- OK\n " \
85
+ "- Remove association {host:#{host_name} <-> group:#{group_names[0]}}...\n " \
86
+ "- OK\n " \
87
+ "- All OK\n" \
108
88
  "Succeeded\n"
109
89
  expected(actual, desired)
110
- # rubocop:enable Metrics/LineLength
111
-
112
90
  # We should have the correct group associations
113
91
  host = @db.models[:host].find(name: host_name)
114
92
  groups = host.groups_dataset
@@ -122,24 +100,21 @@ RSpec.describe Moose::Inventory::Cli::Host do
122
100
  # 1. expect that the group association is removed
123
101
  # 2. expect that an association will be made with 'ungrouped'.
124
102
  actual = runner do
125
- @app.start(args = %W(host rmgroup #{host_name} #{group_names[1]}))
103
+ @app.start(%W[host rmgroup #{host_name} #{group_names[1]} --yes])
126
104
  end
127
105
 
128
- # rubocop:disable Metrics/LineLength
129
106
  desired = { aborted: false }
130
107
  desired[:STDOUT] =
131
- "Dissociate host '#{host_name}' from groups '#{group_names[1]}':\n"\
132
- " - Retrieve host '#{host_name}'...\n"\
133
- " - OK\n"\
134
- " - Remove association {host:#{host_name} <-> group:#{group_names[1]}}...\n"\
135
- " - OK\n"\
136
- " - Add automatic association {host:#{host_name} <-> group:ungrouped}...\n"\
137
- " - OK\n"\
138
- " - All OK\n"\
108
+ "Dissociate host '#{host_name}' from groups '#{group_names[1]}':\n " \
109
+ "- Retrieve host '#{host_name}'...\n " \
110
+ "- OK\n " \
111
+ "- Remove association {host:#{host_name} <-> group:#{group_names[1]}}...\n " \
112
+ "- OK\n " \
113
+ "- Add automatic association {host:#{host_name} <-> group:ungrouped}...\n " \
114
+ "- OK\n " \
115
+ "- All OK\n" \
139
116
  "Succeeded\n"
140
117
  expected(actual, desired)
141
- # rubocop:enable Metrics/LineLength
142
-
143
118
  # We should have the correct group associations
144
119
  host = @db.models[:host].find(name: host_name)
145
120
  groups = host.groups_dataset
@@ -149,6 +124,23 @@ RSpec.describe Moose::Inventory::Cli::Host do
149
124
  expect(groups[name: 'ungrouped']).not_to be_nil
150
125
  end
151
126
 
127
+ #------------------------
128
+ it 'host rmgroup HOST GROUP --dry-run should not remove membership or add ungrouped' do
129
+ host_name = 'test1'
130
+ group_name = 'group1'
131
+ runner { @app.start(%W[host add #{host_name}]) }
132
+ runner { @app.start(%W[host addgroup #{host_name} #{group_name}]) }
133
+
134
+ actual = runner { @app.start(%W[host rmgroup #{host_name} #{group_name} --dry-run]) }
135
+
136
+ expect(actual[:unexpected]).to eq(false)
137
+ expect(actual[:aborted]).to eq(false)
138
+ expect(actual[:STDOUT]).to include('Dry run complete. No changes applied.')
139
+ host = @db.models[:host].find(name: host_name)
140
+ expect(host.groups_dataset[name: group_name]).not_to be_nil
141
+ expect(host.groups_dataset[name: 'ungrouped']).to be_nil
142
+ end
143
+
152
144
  #------------------------
153
145
  it 'host rmgroup HOST GROUP ... should warn about non-existing associations' do
154
146
  # 1. Should warn that the group doesn't exist.
@@ -156,22 +148,21 @@ RSpec.describe Moose::Inventory::Cli::Host do
156
148
 
157
149
  host_name = 'test1'
158
150
  group_name = 'no-group'
159
- runner { @app.start(%W(host add #{host_name})) }
151
+ runner { @app.start(%W[host add #{host_name}]) }
160
152
 
161
153
  actual = runner do
162
- @app.start(%W(host rmgroup #{host_name} #{group_name}))
154
+ @app.start(%W[host rmgroup #{host_name} #{group_name} --yes])
163
155
  end
164
156
 
165
- # rubocop:disable Metrics/LineLength
166
157
  desired = { aborted: false }
167
158
  desired[:STDOUT] =
168
- "Dissociate host '#{host_name}' from groups '#{group_name}':\n"\
169
- " - Retrieve host \'#{host_name}\'...\n"\
170
- " - OK\n"\
171
- " - Remove association {host:#{host_name} <-> group:#{group_name}}...\n"\
172
- " - Doesn't exist, skipping.\n"\
173
- " - OK\n"\
174
- " - All OK\n"\
159
+ "Dissociate host '#{host_name}' from groups '#{group_name}':\n " \
160
+ "- Retrieve host '#{host_name}'...\n " \
161
+ "- OK\n " \
162
+ "- Remove association {host:#{host_name} <-> group:#{group_name}}...\n " \
163
+ "- Doesn't exist, skipping.\n " \
164
+ "- OK\n " \
165
+ "- All OK\n" \
175
166
  "Succeeded\n"
176
167
  desired[:STDERR] = "WARNING: Association {host:#{host_name} <-> group:#{group_name}} doesn't exist, skipping.\n"
177
168
 
@@ -183,9 +174,9 @@ RSpec.describe Moose::Inventory::Cli::Host do
183
174
  name = 'test1'
184
175
  groupname = 'ungrouped'
185
176
 
186
- runner { @app.start(%W(host add #{name})) }
177
+ runner { @app.start(%W[host add #{name}]) }
187
178
 
188
- actual = runner { @app.start(%W(host rmgroup #{name} #{groupname})) }
179
+ actual = runner { @app.start(%W[host rmgroup #{name} #{groupname} --yes]) }
189
180
 
190
181
  desired = { aborted: true }
191
182
  desired[:STDERR] =
@@ -194,41 +185,39 @@ RSpec.describe Moose::Inventory::Cli::Host do
194
185
  end
195
186
 
196
187
  #------------------------
197
- it 'host rmgroup GROUP1 GROUP1 ... should dissociate the host from'\
198
- ' multiple groups at once' do
188
+ it 'host rmgroup GROUP1 GROUP1 ... should dissociate the host from ' \
189
+ 'multiple groups at once' do
199
190
  # 1. Should rm the host to the group
200
191
  # 2. Should add the host from the 'ungrouped' automatic group
201
192
  # if it has no other groups.
202
193
 
203
194
  host_name = 'test1'
204
- runner { @app.start(%W(host add #{host_name})) }
195
+ runner { @app.start(%W[host add #{host_name}]) }
205
196
 
206
- group_names = %w(group1 group2)
197
+ group_names = %w[group1 group2]
207
198
  group_names.each do |group|
208
- runner { @app.start(%W(host addgroup #{host_name} #{group})) }
199
+ runner { @app.start(%W[host addgroup #{host_name} #{group}]) }
209
200
  end
210
201
 
211
202
  actual = runner do
212
- @app.start(%W(host rmgroup #{host_name}) + group_names)
203
+ @app.start(%W[host rmgroup #{host_name} --yes] + group_names)
213
204
  end
214
205
  desired = { aborted: false }
215
206
  desired[:STDOUT] =
216
- "Dissociate host '#{host_name}' from groups '#{group_names.join(',')}':\n"\
217
- " - Retrieve host \'#{host_name}\'...\n"\
218
- " - OK\n"
207
+ "Dissociate host '#{host_name}' from groups '#{group_names.join(',')}':\n " \
208
+ "- Retrieve host '#{host_name}'...\n " \
209
+ "- OK\n"
219
210
  group_names.each do |group|
220
211
  desired[:STDOUT] = desired[:STDOUT] +
221
- " - Remove association {host:#{host_name} <-> group:#{group}}...\n"\
222
- " - OK\n"\
212
+ " - Remove association {host:#{host_name} <-> group:#{group}}...\n " \
213
+ "- OK\n" \
223
214
  end
224
215
  desired[:STDOUT] = desired[:STDOUT] +
225
- " - Add automatic association {host:#{host_name} <-> group:ungrouped}...\n"\
226
- " - OK\n"\
227
- " - All OK\n"\
216
+ " - Add automatic association {host:#{host_name} <-> group:ungrouped}...\n " \
217
+ "- OK\n " \
218
+ "- All OK\n" \
228
219
  "Succeeded\n"
229
220
  expected(actual, desired)
230
- # rubocop:enable Metrics/LineLength
231
-
232
221
  # We should have the correct group associations
233
222
  host = @db.models[:host].find(name: host_name)
234
223
  groups = host.groups_dataset
@@ -240,3 +229,4 @@ RSpec.describe Moose::Inventory::Cli::Host do
240
229
  end
241
230
  end
242
231
  end
232
+ # 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::Host 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
- @host = Moose::Inventory::Cli::Host
29
- @app = Moose::Inventory::Cli::Application
10
+ setup_cli_harness(command_class: Moose::Inventory::Cli::Host, command_ivar: :@host)
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 = @host.instance_methods(false).include?(:rmvar)
19
+ result = @host.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(host rmvar)) # <- no group given
26
+ @app.start(%w[host rmvar]) # <- no group given
46
27
  end
47
28
 
48
29
  # Check output
@@ -56,16 +37,16 @@ RSpec.describe Moose::Inventory::Cli::Host do
56
37
  host_name = 'not-a-host'
57
38
  var_name = 'foo=bar'
58
39
  actual = runner do
59
- @app.start(%W(host rmvar #{host_name} #{var_name}))
40
+ @app.start(%W[host rmvar #{host_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 host '#{host_name}':\n"\
66
- " - retrieve host '#{host_name}'...\n"
46
+ "Remove variable(s) '#{var_name}' from host '#{host_name}':\n " \
47
+ "- retrieve host '#{host_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 host '#{host_name}' does not exist.\n"
70
51
  expected(actual, desired)
71
52
  end
@@ -77,29 +58,27 @@ RSpec.describe Moose::Inventory::Cli::Host do
77
58
 
78
59
  host_name = 'test1'
79
60
  @db.models[:host].create(name: host_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(host rmvar #{host_name} #{args}))
70
+ @app.start(%W[host rmvar #{host_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 host '#{host_name}':\n"\
98
- " - retrieve host '#{host_name}'...\n"\
99
- " - OK\n"\
100
- " - remove variable '#{args}'...\n"
76
+ "Remove variable(s) '#{args}' from host '#{host_name}':\n " \
77
+ "- retrieve host '#{host_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)
@@ -107,6 +86,20 @@ RSpec.describe Moose::Inventory::Cli::Host do
107
86
  end
108
87
 
109
88
  #------------------------
89
+ it 'host rmvar HOST key --dry-run should not remove the host variable' do
90
+ host_name = 'test1'
91
+ @db.models[:host].create(name: host_name)
92
+ runner { @app.start(%W[host addvar #{host_name} var1=val1]) }
93
+
94
+ actual = runner { @app.start(%W[host rmvar #{host_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
+ host = @db.models[:host].find(name: host_name)
100
+ expect(host.hostvars_dataset[name: 'var1']).not_to be_nil
101
+ end
102
+
110
103
  it 'host rmvar HOST <valid args> ... should remove the host variable' do
111
104
  # 1. Should add the var to the db
112
105
  # 2. Should associate the host with the var
@@ -114,11 +107,11 @@ RSpec.describe Moose::Inventory::Cli::Host do
114
107
  host_name = 'test1'
115
108
 
116
109
  var = { name: 'foo', value: 'bar' }
117
- cases = %W(
110
+ cases = %W[
118
111
  #{var[:name]}
119
112
  #{var[:name]}=
120
113
  #{var[:name]}=#{var[:value]}
121
- )
114
+ ]
122
115
  cases.each do |example|
123
116
  # reset the db
124
117
  @db.reset
@@ -126,24 +119,24 @@ RSpec.describe Moose::Inventory::Cli::Host do
126
119
  # Add an initial host and hostvar
127
120
  @db.models[:host].create(name: host_name)
128
121
  runner do
129
- @app.start(%W(host addvar #{host_name} #{var[:name]}=#{var[:value]}))
122
+ @app.start(%W[host addvar #{host_name} #{var[:name]}=#{var[:value]}])
130
123
  end
131
124
 
132
125
  # Try to remove the hostvar using the case example valid args
133
126
  actual = runner do
134
- @app.start(%W(host rmvar #{host_name} #{example}))
127
+ @app.start(%W[host rmvar #{host_name} #{example} --yes])
135
128
  end
136
129
  # @console.out(actual,'p')
137
130
 
138
131
  # Check the output
139
132
  desired = { aborted: false }
140
133
  desired[:STDOUT] =
141
- "Remove variable(s) '#{example}' from host '#{host_name}':\n"\
142
- " - retrieve host '#{host_name}'...\n"\
143
- " - OK\n"\
144
- " - remove variable '#{example}'...\n"\
145
- " - OK\n"\
146
- " - all OK\n"\
134
+ "Remove variable(s) '#{example}' from host '#{host_name}':\n " \
135
+ "- retrieve host '#{host_name}'...\n " \
136
+ "- OK\n " \
137
+ "- remove variable '#{example}'...\n " \
138
+ "- OK\n " \
139
+ "- all OK\n" \
147
140
  "Succeeded.\n"
148
141
 
149
142
  # @console.out(desired,'p')
@@ -164,36 +157,35 @@ RSpec.describe Moose::Inventory::Cli::Host do
164
157
  host_name = 'test1'
165
158
  varsarray = [
166
159
  { name: 'var1', value: 'val1' },
167
- { name: 'var2', value: 'val2' },
160
+ { name: 'var2', value: 'val2' }
168
161
  ]
169
162
 
170
- vars = []
171
- varsarray.each do |var|
172
- vars << "#{var[:name]}=#{var[:value]}"
163
+ vars = varsarray.map do |var|
164
+ "#{var[:name]}=#{var[:value]}"
173
165
  end
174
166
 
175
167
  @db.models[:host].create(name: host_name)
176
- actual = runner do
177
- @app.start(%W(host addvar #{host_name}) + vars)
168
+ runner do
169
+ @app.start(%W[host addvar #{host_name}] + vars)
178
170
  end
179
171
 
180
172
  actual = runner do
181
- @app.start(%W(host rmvar #{host_name}) + vars)
173
+ @app.start(%W[host rmvar #{host_name} --yes] + vars)
182
174
  end
183
175
  # @console.out(actual,'p')
184
176
 
185
177
  desired = { aborted: false }
186
178
  desired[:STDOUT] =
187
- "Remove variable(s) '#{vars.join(',')}' from host '#{host_name}':\n"\
188
- " - retrieve host '#{host_name}'...\n"\
189
- " - OK\n"
179
+ "Remove variable(s) '#{vars.join(',')}' from host '#{host_name}':\n " \
180
+ "- retrieve host '#{host_name}'...\n " \
181
+ "- OK\n"
190
182
  vars.each do |var|
191
183
  desired[:STDOUT] = desired[:STDOUT] +
192
- " - remove variable '#{var}'...\n"\
193
- " - OK\n"
184
+ " - remove variable '#{var}'...\n " \
185
+ "- OK\n"
194
186
  end
195
187
  desired[:STDOUT] = desired[:STDOUT] +
196
- " - all OK\n"\
188
+ " - all OK\n" \
197
189
  "Succeeded.\n"
198
190
  expected(actual, desired)
199
191
 
@@ -204,3 +196,4 @@ RSpec.describe Moose::Inventory::Cli::Host do
204
196
  end
205
197
  end
206
198
  end
199
+ # 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 'host_add_spec'
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'spec_helper'
5
+
6
+ # rubocop:disable Metrics/BlockLength
7
+ RSpec.describe 'host and group metadata tags' do
8
+ before(:all) do
9
+ setup_cli_harness(command_class: Moose::Inventory::Cli::Application)
10
+ end
11
+
12
+ before(:each) do
13
+ reset_cli_harness
14
+ end
15
+
16
+ it 'adds and lists host tags' do
17
+ runner { @app.start(%w[host add app01]) }
18
+
19
+ add = runner { @app.start(%w[host addtag app01 prod critical]) }
20
+ list = runner { @app.start(%w[host listtags app01]) }
21
+
22
+ expect(add[:unexpected]).to eq(false)
23
+ expect(add[:STDOUT]).to include("Added host tag(s) to 'app01': prod, critical.")
24
+ expect(list[:STDOUT]).to eq("Host 'app01' tags: critical, prod\n")
25
+ end
26
+
27
+ it 'normalizes host tag casing and deduplicates values' do
28
+ runner { @app.start(%w[host add app01]) }
29
+
30
+ add = runner { @app.start(%w[host addtag app01 Prod PROD owner-platform]) }
31
+ list = runner { @app.start(%w[host listtags app01]) }
32
+
33
+ expect(add[:unexpected]).to eq(false)
34
+ expect(add[:STDOUT]).to include("Added host tag(s) to 'app01': prod, owner-platform.")
35
+ expect(list[:STDOUT]).to eq("Host 'app01' tags: owner-platform, prod\n")
36
+ expect(@db.models[:tag].where(name: 'Prod').count).to eq(0)
37
+ expect(@db.models[:tag].where(name: 'prod').count).to eq(1)
38
+ end
39
+
40
+ it 'removes host tags' do
41
+ runner { @app.start(%w[host add app01]) }
42
+ runner { @app.start(%w[host addtag app01 prod critical]) }
43
+
44
+ remove = runner { @app.start(%w[host rmtag app01 PROD --yes]) }
45
+ list = runner { @app.start(%w[host listtags app01]) }
46
+
47
+ expect(remove[:unexpected]).to eq(false)
48
+ expect(remove[:STDOUT]).to include("Removed host tag(s) from 'app01': prod.")
49
+ expect(list[:STDOUT]).to eq("Host 'app01' tags: critical\n")
50
+ end
51
+
52
+ it 'adds and lists group tags as JSON' do
53
+ runner { @app.start(%w[group add web]) }
54
+ runner { @app.start(%w[group addtag web frontend owner-platform]) }
55
+
56
+ actual = runner { @app.start(%w[group listtags web --format json]) }
57
+ parsed = JSON.parse(actual[:STDOUT])
58
+
59
+ expect(actual[:unexpected]).to eq(false)
60
+ expect(parsed).to eq('group' => 'web', 'tags' => %w[frontend owner-platform])
61
+ end
62
+
63
+ it 'records audit events for tag changes' do
64
+ runner { @app.start(%w[host add app01]) }
65
+ runner { @app.start(%w[host addtag app01 prod]) }
66
+
67
+ event = @db.models[:audit_event].last
68
+ expect(event.command).to eq('host addtag')
69
+ expect(event.action).to eq('add_tag')
70
+ expect(event.entity_type).to eq('host')
71
+ expect(event.entity_name).to eq('app01')
72
+ end
73
+
74
+ it 'aborts when tagging a missing entity' do
75
+ actual = runner { @app.start(%w[group addtag missing prod]) }
76
+
77
+ expect(actual[:aborted]).to eq(true)
78
+ expect(actual[:STDERR]).to include("ERROR: The group 'missing' does not exist.")
79
+ end
80
+ end
81
+ # rubocop:enable Metrics/BlockLength
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  RSpec.describe 'Moose::Inventory::Config' do
4
6
  before(:all) do
5
7
  # Set up the configuration object
6
8
  @mockarg_parts = {
7
- config: File.join(spec_root, 'config/config.yml'),
8
- format: 'yaml',
9
- env: 'test',
9
+ config: File.join(spec_root, 'config/config.yml'),
10
+ format: 'yaml',
11
+ env: 'test'
10
12
  }
11
13
 
12
14
  @mockargs = []
@@ -24,6 +26,26 @@ RSpec.describe 'Moose::Inventory::Config' do
24
26
  result = @config.respond_to?(:init)
25
27
  expect(result).to eq(true)
26
28
  end
29
+
30
+ it 'resets runtime state before parsing new arguments' do
31
+ @config.init(@mockargs)
32
+ @config._settings[:junk] = true
33
+ @config._argv << '--junk'
34
+
35
+ @config.init(['--config', @mockarg_parts[:config]])
36
+
37
+ expect(@config._settings[:junk]).to be_nil
38
+ expect(@config._argv).not_to include('--junk')
39
+ end
40
+
41
+ it 'builds a runtime options object from the resolved arguments' do
42
+ @config.init(@mockargs)
43
+
44
+ expect(@config.runtime_options.argv).to eq([])
45
+ expect(@config.runtime_options.output_format).to eq('yaml')
46
+ expect(@config.runtime_options.ansible?).to eq(false)
47
+ expect(@config.application_args).to eq([])
48
+ end
27
49
  end
28
50
 
29
51
  # ._configopts
@@ -60,6 +82,22 @@ RSpec.describe 'Moose::Inventory::Config' do
60
82
  end
61
83
  end
62
84
 
85
+ describe 'flag parsing' do
86
+ it 'raises when --config is missing its value' do
87
+ expect { @config.init(['--config']) }.to raise_error(RuntimeError, 'Expected a value after --config')
88
+ end
89
+
90
+ it 'raises when --env is missing its value' do
91
+ expect { @config.init(['--env', '--config', @mockarg_parts[:config]]) }
92
+ .to raise_error(RuntimeError, 'Expected a value after --env')
93
+ end
94
+
95
+ it 'raises when --format is missing its value' do
96
+ expect { @config.init(['--config', @mockarg_parts[:config], '--format']) }
97
+ .to raise_error(RuntimeError, 'Expected a value after --format')
98
+ end
99
+ end
100
+
63
101
  # ._settings
64
102
  describe '._settings' do
65
103
  it 'should be responsive' do